@runnerpro/backend 1.22.8 → 1.23.0
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/lib/cjs/chat/api/conversation.js +5 -4
- package/lib/cjs/index.js +5 -1
- package/lib/cjs/mediaProcessing/audio.js +216 -0
- package/lib/cjs/mediaProcessing/documents.js +225 -0
- package/lib/cjs/mediaProcessing/ffmpegUtils.js +107 -0
- package/lib/cjs/mediaProcessing/image.js +59 -0
- package/lib/cjs/mediaProcessing/index.js +145 -132
- package/lib/cjs/mediaProcessing/video.js +149 -0
- package/lib/cjs/prompt/ai.js +29 -1
- package/lib/cjs/types/chat/api/conversation.d.ts.map +1 -1
- package/lib/cjs/types/index.d.ts +2 -2
- package/lib/cjs/types/index.d.ts.map +1 -1
- package/lib/cjs/types/mediaProcessing/audio.d.ts +63 -0
- package/lib/cjs/types/mediaProcessing/audio.d.ts.map +1 -0
- package/lib/cjs/types/mediaProcessing/documents.d.ts +79 -0
- package/lib/cjs/types/mediaProcessing/documents.d.ts.map +1 -0
- package/lib/cjs/types/mediaProcessing/ffmpegUtils.d.ts +31 -0
- package/lib/cjs/types/mediaProcessing/ffmpegUtils.d.ts.map +1 -0
- package/lib/cjs/types/mediaProcessing/image.d.ts +20 -0
- package/lib/cjs/types/mediaProcessing/image.d.ts.map +1 -0
- package/lib/cjs/types/mediaProcessing/index.d.ts +41 -28
- package/lib/cjs/types/mediaProcessing/index.d.ts.map +1 -1
- package/lib/cjs/types/mediaProcessing/video.d.ts +21 -0
- package/lib/cjs/types/mediaProcessing/video.d.ts.map +1 -0
- package/lib/cjs/types/prompt/ai.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -8,161 +8,180 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.reprocessRecentMedia = exports.processMediaFile = exports.describeImage = exports.transcribeAudio = void 0;
|
|
13
|
-
const
|
|
14
|
-
const index_1 = require("../
|
|
15
|
-
const index_2 = require("../
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'audio/m4a': 'MP3',
|
|
35
|
-
'audio/aac': 'MP3',
|
|
15
|
+
exports.FILE_KIND = exports.reprocessRecentMedia = exports.processMediaFile = exports.analyzeFile = exports.classifyFile = exports.describeOpaqueFile = exports.describeTextFile = exports.describePdf = exports.describeVideo = exports.describeImage = exports.transcribeAudio = void 0;
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const index_1 = require("../db/index");
|
|
18
|
+
const index_2 = require("../err/index");
|
|
19
|
+
const audio_1 = require("./audio");
|
|
20
|
+
Object.defineProperty(exports, "transcribeAudio", { enumerable: true, get: function () { return audio_1.transcribeAudio; } });
|
|
21
|
+
const image_1 = require("./image");
|
|
22
|
+
Object.defineProperty(exports, "describeImage", { enumerable: true, get: function () { return image_1.describeImage; } });
|
|
23
|
+
const video_1 = require("./video");
|
|
24
|
+
Object.defineProperty(exports, "describeVideo", { enumerable: true, get: function () { return video_1.describeVideo; } });
|
|
25
|
+
const documents_1 = require("./documents");
|
|
26
|
+
Object.defineProperty(exports, "describePdf", { enumerable: true, get: function () { return documents_1.describePdf; } });
|
|
27
|
+
Object.defineProperty(exports, "describeTextFile", { enumerable: true, get: function () { return documents_1.describeTextFile; } });
|
|
28
|
+
Object.defineProperty(exports, "describeOpaqueFile", { enumerable: true, get: function () { return documents_1.describeOpaqueFile; } });
|
|
29
|
+
// ✅ Tipos de archivo que sabemos tratar, cada uno con su extractor
|
|
30
|
+
const FILE_KIND = {
|
|
31
|
+
AUDIO: 'AUDIO',
|
|
32
|
+
IMAGE: 'IMAGE',
|
|
33
|
+
VIDEO: 'VIDEO',
|
|
34
|
+
PDF: 'PDF',
|
|
35
|
+
TEXT: 'TEXT',
|
|
36
|
+
OPAQUE: 'OPAQUE',
|
|
36
37
|
};
|
|
38
|
+
exports.FILE_KIND = FILE_KIND;
|
|
39
|
+
// ✅ MIME types que son texto plano aunque no empiecen por 'text/'
|
|
40
|
+
const TEXT_MIME_TYPES = new Set([
|
|
41
|
+
'application/json',
|
|
42
|
+
'application/ld+json',
|
|
43
|
+
'application/x-ndjson',
|
|
44
|
+
'application/xml',
|
|
45
|
+
'application/javascript',
|
|
46
|
+
'application/x-yaml',
|
|
47
|
+
'image/svg+xml', // ⭐ es una imagen, pero el modelo de visión no la renderiza: se lee como XML
|
|
48
|
+
]);
|
|
49
|
+
// ✅ Extensiones de texto, para cuando el cliente sube un .gpx o un .csv como 'application/octet-stream'
|
|
50
|
+
const TEXT_EXTENSIONS = new Set([
|
|
51
|
+
'.txt',
|
|
52
|
+
'.csv',
|
|
53
|
+
'.tsv',
|
|
54
|
+
'.md',
|
|
55
|
+
'.json',
|
|
56
|
+
'.xml',
|
|
57
|
+
'.gpx',
|
|
58
|
+
'.tcx',
|
|
59
|
+
'.log',
|
|
60
|
+
'.srt',
|
|
61
|
+
'.vtt',
|
|
62
|
+
'.yaml',
|
|
63
|
+
'.yml',
|
|
64
|
+
'.html',
|
|
65
|
+
'.htm',
|
|
66
|
+
'.svg',
|
|
67
|
+
]);
|
|
37
68
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* @param fileBuffer - Buffer del archivo de audio
|
|
42
|
-
* @param mimetype - Tipo MIME del archivo (ej: 'audio/mpeg', 'audio/wav')
|
|
43
|
-
* @returns Promise<string> - Transcripción del audio en español
|
|
69
|
+
* Decide qué extractor le toca a un archivo a partir de su MIME type, su nombre
|
|
70
|
+
* y, como último recurso, el contenido del propio buffer.
|
|
44
71
|
*
|
|
45
|
-
* @
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* ```
|
|
72
|
+
* @param mimetype - Tipo MIME declarado por el cliente
|
|
73
|
+
* @param filename - Nombre original del archivo
|
|
74
|
+
* @param fileBuffer - Buffer del archivo
|
|
75
|
+
* @returns Tipo de tratamiento a aplicar
|
|
50
76
|
*/
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
enableAutomaticPunctuation: true, // Puntuación automática
|
|
69
|
-
model: 'latest_long', // Modelo más reciente para audios largos
|
|
70
|
-
useEnhanced: true, // Modelo mejorado
|
|
71
|
-
},
|
|
72
|
-
});
|
|
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;
|
|
77
|
+
const classifyFile = (mimetype, filename, fileBuffer) => {
|
|
78
|
+
const mime = (mimetype || '').toLowerCase().split(';')[0].trim();
|
|
79
|
+
const extension = path_1.default.extname(filename || '').toLowerCase();
|
|
80
|
+
if (!(fileBuffer === null || fileBuffer === void 0 ? void 0 : fileBuffer.length))
|
|
81
|
+
return FILE_KIND.OPAQUE;
|
|
82
|
+
// ⭐ El SVG se comprueba antes que 'image/' porque hay que leerlo como texto
|
|
83
|
+
if (mime === 'image/svg+xml' || extension === '.svg')
|
|
84
|
+
return FILE_KIND.TEXT;
|
|
85
|
+
if (mime.startsWith('audio/'))
|
|
86
|
+
return FILE_KIND.AUDIO;
|
|
87
|
+
if (mime.startsWith('image/'))
|
|
88
|
+
return FILE_KIND.IMAGE;
|
|
89
|
+
if (mime.startsWith('video/'))
|
|
90
|
+
return FILE_KIND.VIDEO;
|
|
91
|
+
// ⭐ El PDF va entero al modelo, salvo que sea demasiado grande para mandarlo inline
|
|
92
|
+
if (mime === 'application/pdf' || extension === '.pdf') {
|
|
93
|
+
return fileBuffer.length <= documents_1.MAX_MODEL_FILE_BYTES ? FILE_KIND.PDF : FILE_KIND.OPAQUE;
|
|
79
94
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.error('[transcribeAudio] Error Speech-to-Text:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
83
|
-
(0, index_3.err)(null, null, error, null);
|
|
84
|
-
return '[Error al transcribir audio]';
|
|
95
|
+
if (mime.startsWith('text/') || TEXT_MIME_TYPES.has(mime) || mime.endsWith('+xml') || mime.endsWith('+json')) {
|
|
96
|
+
return FILE_KIND.TEXT;
|
|
85
97
|
}
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
if (TEXT_EXTENSIONS.has(extension))
|
|
99
|
+
return FILE_KIND.TEXT;
|
|
100
|
+
// ⭐ Último recurso: si el contenido parece texto legible, lo tratamos como tal
|
|
101
|
+
if ((0, documents_1.looksLikeText)(fileBuffer))
|
|
102
|
+
return FILE_KIND.TEXT;
|
|
103
|
+
return FILE_KIND.OPAQUE;
|
|
104
|
+
};
|
|
105
|
+
exports.classifyFile = classifyFile;
|
|
88
106
|
/**
|
|
89
|
-
*
|
|
90
|
-
* Contextualizada para una aplicación de fitness/entrenamiento
|
|
107
|
+
* Analiza cualquier adjunto y devuelve su representación en texto.
|
|
91
108
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
109
|
+
* Nunca devuelve vacío: los tipos cuyo contenido no se puede extraer (comprimidos,
|
|
110
|
+
* binarios propietarios, ofimática antigua) reciben una ficha con nombre, tipo y tamaño.
|
|
111
|
+
* Esto importa porque el entrenador se salta la respuesta si un adjunto no tiene texto,
|
|
112
|
+
* y un archivo no soportado no debe distinguirse de uno que aún no se ha procesado.
|
|
113
|
+
*
|
|
114
|
+
* @param fileBuffer - Buffer del archivo
|
|
115
|
+
* @param mimetype - Tipo MIME del archivo
|
|
116
|
+
* @param filename - Nombre original del archivo
|
|
117
|
+
* @returns Promise<string> - Transcripción, descripción, resumen o ficha del archivo
|
|
95
118
|
*
|
|
96
119
|
* @example
|
|
97
120
|
* ```typescript
|
|
98
|
-
*
|
|
99
|
-
* //
|
|
121
|
+
* await analyzeFile(pdfBuffer, 'application/pdf', 'analitica.pdf');
|
|
122
|
+
* // "Analítica de sangre del 3 de junio con ferritina de 28 ng/ml..."
|
|
100
123
|
* ```
|
|
101
124
|
*/
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
const analyzeFile = (fileBuffer, mimetype, filename) => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
const kind = classifyFile(mimetype, filename, fileBuffer);
|
|
127
|
+
const fallback = () => (0, documents_1.describeOpaqueFile)(mimetype, filename, (fileBuffer === null || fileBuffer === void 0 ? void 0 : fileBuffer.length) || 0);
|
|
128
|
+
let fileText;
|
|
129
|
+
switch (kind) {
|
|
130
|
+
case FILE_KIND.AUDIO:
|
|
131
|
+
fileText = yield (0, audio_1.transcribeAudio)(fileBuffer, mimetype, filename);
|
|
132
|
+
break;
|
|
133
|
+
case FILE_KIND.IMAGE:
|
|
134
|
+
fileText = yield (0, image_1.describeImage)(fileBuffer, mimetype);
|
|
135
|
+
break;
|
|
136
|
+
case FILE_KIND.VIDEO:
|
|
137
|
+
fileText = yield (0, video_1.describeVideo)(fileBuffer, mimetype, filename);
|
|
138
|
+
break;
|
|
139
|
+
case FILE_KIND.PDF:
|
|
140
|
+
fileText = yield (0, documents_1.describePdf)(fileBuffer, filename);
|
|
141
|
+
break;
|
|
142
|
+
case FILE_KIND.TEXT:
|
|
143
|
+
fileText = yield (0, documents_1.describeTextFile)(fileBuffer, filename);
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
fileText = fallback();
|
|
123
147
|
}
|
|
148
|
+
// ⭐ Postgres rechaza el carácter nulo en columnas text. Si se colara uno, el UPDATE fallaría
|
|
149
|
+
// sin avisar (query() se traga el error) y el adjunto se quedaría sin texto para siempre,
|
|
150
|
+
// que es justo el estado que bloquea al entrenador.
|
|
151
|
+
const clean = (fileText || '').split('\u0000').join('').trim();
|
|
152
|
+
return clean || fallback();
|
|
124
153
|
});
|
|
125
|
-
exports.
|
|
154
|
+
exports.analyzeFile = analyzeFile;
|
|
126
155
|
/**
|
|
127
|
-
* Procesa un
|
|
128
|
-
*
|
|
129
|
-
* - Imagen: usa Gemini (Vertex AI)
|
|
156
|
+
* Procesa un adjunto de chat y guarda su texto en la base de datos.
|
|
157
|
+
* Pensado para llamarse sin await (fire-and-forget) desde el envío del archivo.
|
|
130
158
|
*
|
|
131
159
|
* @param idMessage - ID del mensaje en CHAT MESSAGE
|
|
132
160
|
* @param fileBuffer - Buffer del archivo
|
|
133
161
|
* @param mimetype - Tipo MIME del archivo
|
|
162
|
+
* @param filename - Nombre original del archivo
|
|
134
163
|
* @returns Promise<void>
|
|
135
164
|
*
|
|
136
165
|
* @example
|
|
137
166
|
* ```typescript
|
|
138
167
|
* // No usar await para ejecutar en background
|
|
139
|
-
* processMediaFile(123, fileBuffer, '
|
|
168
|
+
* processMediaFile(123, fileBuffer, 'application/pdf', 'informe.pdf');
|
|
140
169
|
* ```
|
|
141
170
|
*/
|
|
142
|
-
const processMediaFile = (idMessage, fileBuffer, mimetype) => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
const processMediaFile = (idMessage, fileBuffer, mimetype, filename) => __awaiter(void 0, void 0, void 0, function* () {
|
|
143
172
|
try {
|
|
144
|
-
|
|
145
|
-
if (mimetype.startsWith('audio/')) {
|
|
146
|
-
fileText = yield transcribeAudio(fileBuffer, mimetype);
|
|
147
|
-
}
|
|
148
|
-
else if (mimetype.startsWith('image/')) {
|
|
149
|
-
fileText = yield describeImage(fileBuffer, mimetype);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
// ❌ Tipo de archivo no soportado para procesamiento
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
173
|
+
const fileText = yield analyzeFile(fileBuffer, mimetype, filename);
|
|
155
174
|
// ✅ Guardar el resultado en la base de datos
|
|
156
|
-
yield (0,
|
|
175
|
+
yield (0, index_1.query)('UPDATE [CHAT MESSAGE] SET [FILE TEXT] = ? WHERE [ID] = ?', [fileText, idMessage]);
|
|
157
176
|
}
|
|
158
177
|
catch (error) {
|
|
159
|
-
(0,
|
|
178
|
+
(0, index_2.err)(null, null, error, null);
|
|
160
179
|
}
|
|
161
180
|
});
|
|
162
181
|
exports.processMediaFile = processMediaFile;
|
|
163
182
|
/**
|
|
164
|
-
* Reprocesa
|
|
165
|
-
* Útil para
|
|
183
|
+
* Reprocesa los adjuntos recientes (últimos 2 días) que no tienen FILE TEXT,
|
|
184
|
+
* sea cual sea su tipo. Útil para rellenar mensajes anteriores al deploy.
|
|
166
185
|
*
|
|
167
186
|
* @param bucket - Instancia del bucket de Google Cloud Storage
|
|
168
187
|
* @returns Promise<{ processed: number; errors: number }> - Estadísticas del reprocesamiento
|
|
@@ -177,11 +196,11 @@ const reprocessRecentMedia = (bucket) => __awaiter(void 0, void 0, void 0, funct
|
|
|
177
196
|
let processed = 0;
|
|
178
197
|
let errors = 0;
|
|
179
198
|
try {
|
|
180
|
-
// ✅
|
|
181
|
-
const messages = yield (0,
|
|
199
|
+
// ✅ Cualquier adjunto sin FILE TEXT, no solo audio e imagen
|
|
200
|
+
const messages = yield (0, index_1.query)(`SELECT "ID", "MIMETYPE", "TEXT" FROM "CHAT MESSAGE"
|
|
182
201
|
WHERE "DATE" >= NOW() - INTERVAL '2 days'
|
|
183
202
|
AND "FILE TEXT" IS NULL
|
|
184
|
-
AND
|
|
203
|
+
AND "MIMETYPE" IS NOT NULL
|
|
185
204
|
ORDER BY "DATE" DESC`, []);
|
|
186
205
|
// eslint-disable-next-line no-console
|
|
187
206
|
console.log(`[reprocessRecentMedia] Encontrados ${messages.length} mensajes para procesar`);
|
|
@@ -196,16 +215,10 @@ const reprocessRecentMedia = (bucket) => __awaiter(void 0, void 0, void 0, funct
|
|
|
196
215
|
continue;
|
|
197
216
|
}
|
|
198
217
|
const [fileBuffer] = yield bucket.file(`Chat/${message.id}`).download();
|
|
199
|
-
// ⭐
|
|
200
|
-
|
|
201
|
-
if (message.mimetype.startsWith('audio/')) {
|
|
202
|
-
fileText = yield transcribeAudio(fileBuffer, message.mimetype);
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
fileText = yield describeImage(fileBuffer, message.mimetype);
|
|
206
|
-
}
|
|
218
|
+
// ⭐ Mismo router que en el envío en vivo
|
|
219
|
+
const fileText = yield analyzeFile(fileBuffer, message.mimetype, message.text);
|
|
207
220
|
// ⭐ Guardar en BD
|
|
208
|
-
yield (0,
|
|
221
|
+
yield (0, index_1.query)('UPDATE [CHAT MESSAGE] SET [FILE TEXT] = ? WHERE [ID] = ?', [fileText, message.id]);
|
|
209
222
|
// eslint-disable-next-line no-console
|
|
210
223
|
console.log(`[reprocessRecentMedia] ✅ Procesado mensaje ${message.id}: ${fileText.substring(0, 50)}...`);
|
|
211
224
|
processed++;
|
|
@@ -218,7 +231,7 @@ const reprocessRecentMedia = (bucket) => __awaiter(void 0, void 0, void 0, funct
|
|
|
218
231
|
}
|
|
219
232
|
}
|
|
220
233
|
catch (error) {
|
|
221
|
-
(0,
|
|
234
|
+
(0, index_2.err)(null, null, error, null);
|
|
222
235
|
}
|
|
223
236
|
return { processed, errors };
|
|
224
237
|
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.describeVideo = void 0;
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const ffmpegUtils_1 = require("./ffmpegUtils");
|
|
19
|
+
const image_1 = require("./image");
|
|
20
|
+
const audio_1 = require("./audio");
|
|
21
|
+
const index_1 = require("../err/index");
|
|
22
|
+
// ✅ La pista se manda como LINEAR16, y con un encoding declarado de verdad Speech-to-Text
|
|
23
|
+
// aplica su límite de 60s de forma estricta (ver el aviso en audio.ts). Se recorta con margen.
|
|
24
|
+
const AUDIO_TRACK_MAX_SECONDS = 55;
|
|
25
|
+
const VIDEO_FRAME_PROMPT = `Esto es un fotograma extraído de un vídeo enviado en un chat de entrenamiento.
|
|
26
|
+
Describe lo que se ve de forma directa en un solo párrafo continuo, sin introducciones ni listas.
|
|
27
|
+
Si es contenido deportivo (gesto de carrera, ejercicio de fuerza, zona del cuerpo dolorida, pantalla de un reloj, entorno de entrenamiento, etc.), céntrate en lo que sirve para valorar la técnica, el estado del atleta o su entrenamiento.
|
|
28
|
+
Devuelve ÚNICAMENTE la descripción en español. Máximo 250 palabras.`;
|
|
29
|
+
/**
|
|
30
|
+
* Extrae un fotograma representativo (al 10% de la duración, entre 1s y 10s)
|
|
31
|
+
*
|
|
32
|
+
* @param inputPath - Ruta del vídeo en disco
|
|
33
|
+
* @param outputPath - Ruta destino del JPEG
|
|
34
|
+
* @param duration - Duración del vídeo en segundos
|
|
35
|
+
*/
|
|
36
|
+
const extractKeyFrame = (inputPath, outputPath, duration) => {
|
|
37
|
+
// ⭐ Buscar el segundo 1 en un clip de medio segundo deja el JPEG sin escribir: en vídeos muy
|
|
38
|
+
// cortos (o de duración desconocida) se coge el primer fotograma
|
|
39
|
+
const timestamp = duration > 1.2 ? Math.min(Math.max(duration * 0.1, 1), 10) : 0;
|
|
40
|
+
return (0, ffmpegUtils_1.runFfmpeg)((0, ffmpegUtils_1.ffmpeg)(inputPath)
|
|
41
|
+
.seekInput(timestamp)
|
|
42
|
+
.outputOptions(['-frames:v', '1', '-vf', "scale='min(1280,iw)':-2", '-q:v', '3'])
|
|
43
|
+
.output(outputPath), 'fotograma');
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Extrae la pista de audio como WAV mono 16kHz, el formato que espera Speech-to-Text
|
|
47
|
+
*
|
|
48
|
+
* @param inputPath - Ruta del vídeo en disco
|
|
49
|
+
* @param outputPath - Ruta destino del WAV
|
|
50
|
+
*/
|
|
51
|
+
const extractAudioTrack = (inputPath, outputPath) => (0, ffmpegUtils_1.runFfmpeg)((0, ffmpegUtils_1.ffmpeg)(inputPath)
|
|
52
|
+
.noVideo()
|
|
53
|
+
.audioCodec('pcm_s16le')
|
|
54
|
+
.audioFrequency(ffmpegUtils_1.SPEECH_SAMPLE_RATE)
|
|
55
|
+
.audioChannels(1)
|
|
56
|
+
.duration(AUDIO_TRACK_MAX_SECONDS)
|
|
57
|
+
.format('wav')
|
|
58
|
+
.output(outputPath), 'audio');
|
|
59
|
+
/**
|
|
60
|
+
* Formatea la duración de un vídeo en español ("1 min 12 s")
|
|
61
|
+
*
|
|
62
|
+
* @param seconds - Duración en segundos
|
|
63
|
+
* @returns Duración legible, o cadena vacía si no se conoce
|
|
64
|
+
*/
|
|
65
|
+
const formatDuration = (seconds) => {
|
|
66
|
+
if (!seconds || seconds <= 0)
|
|
67
|
+
return '';
|
|
68
|
+
const total = Math.round(seconds);
|
|
69
|
+
if (total < 60)
|
|
70
|
+
return `${total} s`;
|
|
71
|
+
const minutes = Math.floor(total / 60);
|
|
72
|
+
const rest = total % 60;
|
|
73
|
+
return rest ? `${minutes} min ${rest} s` : `${minutes} min`;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Analiza un vídeo descomponiéndolo en lo que sí sabemos leer: un fotograma
|
|
77
|
+
* representativo (descrito por el modelo de visión) y su pista de audio
|
|
78
|
+
* (transcrita con Speech-to-Text). Ambas partes son opcionales: un vídeo mudo
|
|
79
|
+
* devuelve solo la descripción visual, y uno sin imagen solo la transcripción.
|
|
80
|
+
*
|
|
81
|
+
* @param fileBuffer - Buffer del vídeo
|
|
82
|
+
* @param mimetype - Tipo MIME del archivo (ej: 'video/mp4')
|
|
83
|
+
* @param filename - Nombre original del archivo
|
|
84
|
+
* @returns Promise<string> - Descripción combinada del vídeo
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const description = await describeVideo(videoBuffer, 'video/mp4', 'tecnica.mp4');
|
|
89
|
+
* // "Vídeo de 12 s. Imagen: corredor de perfil sobre cinta... Audio: mira cómo apoyo el talón..."
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
const describeVideo = (fileBuffer, mimetype, filename) => __awaiter(void 0, void 0, void 0, function* () {
|
|
93
|
+
try {
|
|
94
|
+
return yield (0, ffmpegUtils_1.withTempDir)((dir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
// ffmpeg detecta el formato por contenido, así que la extensión solo es cosmética
|
|
96
|
+
const extension = path_1.default.extname(filename || '') || '.mp4';
|
|
97
|
+
const inputPath = path_1.default.join(dir, `input${extension}`);
|
|
98
|
+
const framePath = path_1.default.join(dir, 'frame.jpg');
|
|
99
|
+
const audioPath = path_1.default.join(dir, 'audio.wav');
|
|
100
|
+
fs_1.default.writeFileSync(inputPath, fileBuffer);
|
|
101
|
+
const { duration, hasAudio, hasVideo } = yield (0, ffmpegUtils_1.probeMedia)(inputPath);
|
|
102
|
+
// ⭐ Fotograma y pista de audio se extraen en paralelo; cada rama falla por su cuenta
|
|
103
|
+
const [frameBuffer, audioBuffer] = yield Promise.all([
|
|
104
|
+
hasVideo
|
|
105
|
+
? extractKeyFrame(inputPath, framePath, duration)
|
|
106
|
+
.then(() => fs_1.default.readFileSync(framePath))
|
|
107
|
+
.catch((error) => {
|
|
108
|
+
// eslint-disable-next-line no-console
|
|
109
|
+
console.error('[describeVideo] No se pudo extraer el fotograma:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
110
|
+
return null;
|
|
111
|
+
})
|
|
112
|
+
: Promise.resolve(null),
|
|
113
|
+
hasAudio
|
|
114
|
+
? extractAudioTrack(inputPath, audioPath)
|
|
115
|
+
.then(() => fs_1.default.readFileSync(audioPath))
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
console.error('[describeVideo] No se pudo extraer el audio:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
119
|
+
return null;
|
|
120
|
+
})
|
|
121
|
+
: Promise.resolve(null),
|
|
122
|
+
]);
|
|
123
|
+
const [imageText, audioText] = yield Promise.all([
|
|
124
|
+
frameBuffer ? (0, image_1.describeImage)(frameBuffer, 'image/jpeg', VIDEO_FRAME_PROMPT) : Promise.resolve(null),
|
|
125
|
+
// ⭐ La pista ya sale de ffmpeg en WAV mono 16 kHz, así que no hay que volver a convertirla
|
|
126
|
+
audioBuffer ? (0, audio_1.transcribeWav16k)(audioBuffer) : Promise.resolve(null),
|
|
127
|
+
]);
|
|
128
|
+
const header = `Vídeo${formatDuration(duration) ? ` de ${formatDuration(duration)}` : ''}`;
|
|
129
|
+
const parts = [];
|
|
130
|
+
if (imageText)
|
|
131
|
+
parts.push(`Imagen: ${imageText}`);
|
|
132
|
+
if (audioText)
|
|
133
|
+
parts.push(`Audio: ${audioText}`);
|
|
134
|
+
if (duration > AUDIO_TRACK_MAX_SECONDS && audioText) {
|
|
135
|
+
parts.push(`(Solo se transcribieron los primeros ${AUDIO_TRACK_MAX_SECONDS} s de audio.)`);
|
|
136
|
+
}
|
|
137
|
+
if (!parts.length)
|
|
138
|
+
return `[${header} sin contenido analizable]`;
|
|
139
|
+
return `${header}. ${parts.join(' ')}`;
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.error('[describeVideo] Error:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
145
|
+
(0, index_1.err)(null, null, error, null);
|
|
146
|
+
return '[Error al analizar vídeo]';
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
exports.describeVideo = describeVideo;
|
package/lib/cjs/prompt/ai.js
CHANGED
|
@@ -278,12 +278,40 @@ function textPromptSignature(options) {
|
|
|
278
278
|
if (!Array.isArray(options === null || options === void 0 ? void 0 : options.messages))
|
|
279
279
|
return undefined;
|
|
280
280
|
try {
|
|
281
|
-
return JSON.stringify(options.messages, (_k, v) =>
|
|
281
|
+
return JSON.stringify(options.messages, (_k, v) => {
|
|
282
|
+
const compacted = compactBinaryForSignature(v);
|
|
283
|
+
if (compacted !== v)
|
|
284
|
+
return compacted;
|
|
285
|
+
return typeof v === 'string' && v.length > TEXT_SIG_MAX_CHARS ? `${v.slice(0, TEXT_SIG_MAX_CHARS)}…(${v.length})` : v;
|
|
286
|
+
});
|
|
282
287
|
}
|
|
283
288
|
catch (_a) {
|
|
284
289
|
return undefined;
|
|
285
290
|
}
|
|
286
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Sustituye los adjuntos binarios por una marca de una línea antes de firmar la llamada.
|
|
294
|
+
*
|
|
295
|
+
* Sin esto, un `Buffer` en un `file part` se serializa byte a byte como
|
|
296
|
+
* `{"type":"Buffer","data":[200,…]}`, a ~4,2 caracteres por byte: un PDF de 3 MB produce una firma
|
|
297
|
+
* de 12,5 M de caracteres que se hashea y se INSERTa entera en LLM CACHE. El recorte de strings de
|
|
298
|
+
* arriba no lo cubre porque el valor ya no es un string. Como `JSON.stringify` invoca `toJSON()`
|
|
299
|
+
* ANTES que el replacer, aquí llega la forma ya convertida, no el Buffer original.
|
|
300
|
+
*
|
|
301
|
+
* @param value - Valor que el replacer está serializando
|
|
302
|
+
* @returns Marca compacta si es binario, o el mismo valor sin tocar
|
|
303
|
+
*/
|
|
304
|
+
function compactBinaryForSignature(value) {
|
|
305
|
+
if (!value || typeof value !== 'object')
|
|
306
|
+
return value;
|
|
307
|
+
if (value.type === 'Buffer' && Array.isArray(value.data))
|
|
308
|
+
return `…(binario ${value.data.length} bytes)`;
|
|
309
|
+
if (ArrayBuffer.isView(value))
|
|
310
|
+
return `…(binario ${value.byteLength} bytes)`;
|
|
311
|
+
if (value instanceof ArrayBuffer)
|
|
312
|
+
return `…(binario ${value.byteLength} bytes)`;
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
287
315
|
/**
|
|
288
316
|
* Registra en "LLM CACHE" el coste de una llamada de `generateText`.
|
|
289
317
|
*
|
|
@@ -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;AAoGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,uBAAuB,qCAAkC,GAAG,iBAwCjE,CAAC;AAmGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,WAAW,0EAAuE,GAAG,kBA0E1F,CAAC;AAEF,QAAA,MAAM,gBAAgB;;;;mBAqBrB,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;AAoGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,uBAAuB,qCAAkC,GAAG,iBAwCjE,CAAC;AAmGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,WAAW,0EAAuE,GAAG,kBA0E1F,CAAC;AAEF,QAAA,MAAM,gBAAgB;;;;mBAqBrB,CAAC;AA2TF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC"}
|
package/lib/cjs/types/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { saveDoneStructuraWorkout } from './workout/estructuraWorkout';
|
|
|
22
22
|
import { getZone } from './workout/paceZone';
|
|
23
23
|
import { saveResponseTime } from './chat/saveResponseTime';
|
|
24
24
|
import { updateSenderView } from './chat/api/conversation';
|
|
25
|
-
import { transcribeAudio, describeImage, processMediaFile, reprocessRecentMedia } from './mediaProcessing';
|
|
25
|
+
import { transcribeAudio, describeImage, describeVideo, describePdf, describeTextFile, analyzeFile, processMediaFile, reprocessRecentMedia } from './mediaProcessing';
|
|
26
26
|
import { generateObject, generateText, createGoogleModel, createGoogleModelFromString, createBedrockModelFromString, createAzureModelFromString, createModelFromString, runWithCostTracking, runWithModels, MODEL_TIER, AI_MODELS, GOOGLE_MODELS, AZURE_PREFIX, AZURE_GPT_5_5_DEPLOYMENT, AZURE_GPT_5_4_MINI_DEPLOYMENT, AZURE_GPT_5_4_NANO_DEPLOYMENT, AZURE_GPT_5_5, AZURE_GPT_5_4_MINI, AZURE_GPT_5_4_NANO, AZURE_PRIMARY_MODELS, PRODUCTION_MODELS, FALLBACK_MODELS, BEDROCK_CLAUDE_OPUS, BEDROCK_CLAUDE_SONNET, BEDROCK_CLAUDE_OPUS_4_5, BEDROCK_CLAUDE_OPUS_4_1, BEDROCK_CLAUDE_SONNET_4_5, BEDROCK_CLAUDE_SONNET_4, BEDROCK_CLAUDE_HAIKU, AISTUDIO_PREFIX, MODEL_PRICING, calculateCost } from './prompt';
|
|
27
|
-
export { sendNotification, sleep, query, queryWithClient, longRunningQuery, batchQuery, toPgArray, pool, sendMail, err, fetchIA, notifySlack, CHANNEL_SLACK, translate, LANGUAGES, useTranslation, getExerciseTranslatedDescription, chat, chatApi, chatExposed, getCountNotificaciones, notionGetUsers, notionGetDatabase, notionAddPage, notionEditPage, NOTION_DATABASES_ID, readSheet, findCellByValue, writeSheet, appendSheet, getNumberByLetter, getLetter, reduceSizeImage, generateShareMap, getDefaultWorkoutImage, sendWorkoutToWatch, getPlanificacionPrueba7dias, saveWorkoutAplication, saveWorkoutLaps, evaluateAchievements, evaluateAchievementsBounded, getUnlockedAchievements, computeStreakStats, saveDoneStructuraWorkout, getZone, saveResponseTime, updateSenderView, transcribeAudio, describeImage, processMediaFile, reprocessRecentMedia, generateObject, generateText, createGoogleModel, createGoogleModelFromString, createBedrockModelFromString, createAzureModelFromString, createModelFromString, runWithCostTracking, runWithModels, MODEL_TIER, AI_MODELS, GOOGLE_MODELS, AZURE_PREFIX, AZURE_GPT_5_5_DEPLOYMENT, AZURE_GPT_5_4_MINI_DEPLOYMENT, AZURE_GPT_5_4_NANO_DEPLOYMENT, AZURE_GPT_5_5, AZURE_GPT_5_4_MINI, AZURE_GPT_5_4_NANO, AZURE_PRIMARY_MODELS, PRODUCTION_MODELS, FALLBACK_MODELS, BEDROCK_CLAUDE_OPUS, BEDROCK_CLAUDE_SONNET, BEDROCK_CLAUDE_OPUS_4_5, BEDROCK_CLAUDE_OPUS_4_1, BEDROCK_CLAUDE_SONNET_4_5, BEDROCK_CLAUDE_SONNET_4, BEDROCK_CLAUDE_HAIKU, AISTUDIO_PREFIX, MODEL_PRICING, calculateCost, };
|
|
27
|
+
export { sendNotification, sleep, query, queryWithClient, longRunningQuery, batchQuery, toPgArray, pool, sendMail, err, fetchIA, notifySlack, CHANNEL_SLACK, translate, LANGUAGES, useTranslation, getExerciseTranslatedDescription, chat, chatApi, chatExposed, getCountNotificaciones, notionGetUsers, notionGetDatabase, notionAddPage, notionEditPage, NOTION_DATABASES_ID, readSheet, findCellByValue, writeSheet, appendSheet, getNumberByLetter, getLetter, reduceSizeImage, generateShareMap, getDefaultWorkoutImage, sendWorkoutToWatch, getPlanificacionPrueba7dias, saveWorkoutAplication, saveWorkoutLaps, evaluateAchievements, evaluateAchievementsBounded, getUnlockedAchievements, computeStreakStats, saveDoneStructuraWorkout, getZone, saveResponseTime, updateSenderView, transcribeAudio, describeImage, describeVideo, describePdf, describeTextFile, analyzeFile, processMediaFile, reprocessRecentMedia, generateObject, generateText, createGoogleModel, createGoogleModelFromString, createBedrockModelFromString, createAzureModelFromString, createModelFromString, runWithCostTracking, runWithModels, MODEL_TIER, AI_MODELS, GOOGLE_MODELS, AZURE_PREFIX, AZURE_GPT_5_5_DEPLOYMENT, AZURE_GPT_5_4_MINI_DEPLOYMENT, AZURE_GPT_5_4_NANO_DEPLOYMENT, AZURE_GPT_5_5, AZURE_GPT_5_4_MINI, AZURE_GPT_5_4_NANO, AZURE_PRIMARY_MODELS, PRODUCTION_MODELS, FALLBACK_MODELS, BEDROCK_CLAUDE_OPUS, BEDROCK_CLAUDE_SONNET, BEDROCK_CLAUDE_OPUS_4_5, BEDROCK_CLAUDE_OPUS_4_1, BEDROCK_CLAUDE_SONNET_4_5, BEDROCK_CLAUDE_SONNET_4, BEDROCK_CLAUDE_HAIKU, AISTUDIO_PREFIX, MODEL_PRICING, calculateCost, };
|
|
28
28
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,gCAAgC,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACjH,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAClH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAIhI,OAAO,KAAK,mBAAmB,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"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"}
|