From 1f8498e5a7d805896c4c90da6f0a438383a66e51 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Mon, 24 Aug 2026 16:32:31 -0500 Subject: [PATCH] feat(ia): analisis de encuadre inteligente y deteccion de texto para evitar cortes en imagenes --- src/app/api/webhooks/whatsapp/route.ts | 24 ++++++++++++++++++------ src/lib/gemini.ts | 16 ++++++++++++++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/app/api/webhooks/whatsapp/route.ts b/src/app/api/webhooks/whatsapp/route.ts index a9e77b5..b425fbb 100644 --- a/src/app/api/webhooks/whatsapp/route.ts +++ b/src/app/api/webhooks/whatsapp/route.ts @@ -267,12 +267,18 @@ function isDuplicateMessage(chatId: string, text: string): boolean { // Determinar dimensiones según el tipo de publicación // Noticias: 640x360 (16:9) | Clasificados y Eventos: 640x640 (1:1) - const isSquare = parsed.tipo === 'evento' || parsed.tipo === 'aviso'; + // Determinar gravedad de encuadre y dimensiones según análisis de IA + const gravity = parsed.crop_focus === 'top' ? 'top' + : parsed.crop_focus === 'bottom' ? 'bottom' + : 'center'; + + // Si es un afiche/flyer con texto integrado o evento/aviso, usar formato cuadrado 640x640 para evitar mutilar texto + const isSquare = parsed.tipo === 'evento' || parsed.tipo === 'aviso' || Boolean(parsed.has_text_overlay || parsed.crop_focus === 'contain'); const imgHeight = isSquare ? 640 : 360; // Notificación de Progreso 2: Estructuración completa y registro (asíncrona) const mediaStatus = candidateMediaList.length > 0 - ? `🖼️ *Imagen:* Optimizando foto destacada (${isSquare ? 'Formato 640x640' : 'Formato 640x360'})` + ? `🖼️ *Imagen:* Optimizando foto destacada (${isSquare ? 'Formato 640x640' : 'Formato 640x360'} • Gravedad: ${gravity})` : `📄 *Formato:* Publicación en texto`; const progressMsg = `⚙️ *Procesando Publicación*\n` @@ -331,7 +337,7 @@ function isDuplicateMessage(chatId: string, text: string): boolean { let ogImageUrl = undefined; if (storageFileId) { - ogImageUrl = `${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/${APPWRITE_ALMEIRA_CONFIG.mediaBucketId}/files/${storageFileId}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}&width=640&height=${imgHeight}&quality=85`; + ogImageUrl = `${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/${APPWRITE_ALMEIRA_CONFIG.mediaBucketId}/files/${storageFileId}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}&width=640&height=${imgHeight}&gravity=${gravity}&quality=85`; } // Determinar fecha de vencimiento @@ -390,14 +396,20 @@ function isDuplicateMessage(chatId: string, text: string): boolean { // Formatear contenido limpio para WhatsApp (sin etiquetas HTML) const cleanContent = htmlToWhatsAppText(parsed.contenido); - // Estado de la imagen destacada + // Estado de la imagen destacada con análisis de encuadre let imageNotice = 'No se adjuntó imagen'; + const focusLabel = parsed.crop_focus === 'top' ? 'Enfoque superior (preserva titulares/rostros)' + : parsed.crop_focus === 'contain' ? 'Formato completo (afiche/texto sin cortes)' + : parsed.crop_focus === 'bottom' ? 'Enfoque inferior' + : 'Enfoque centrado equilibrado'; + if (candidateMediaList.length > 1) { const chosenNum = (parsed.selected_image_index ?? 0) + 1; imageNotice = `Foto #${chosenNum} de ${candidateMediaList.length} analizadas` - + (parsed.image_selection_reason ? ` (${parsed.image_selection_reason})` : ''); + + (parsed.image_selection_reason ? ` (${parsed.image_selection_reason})` : '') + + `\n 📐 _${focusLabel}_`; } else if (candidateMediaList.length === 1) { - imageNotice = `1 foto procesada (${isSquare ? '640x640' : '640x360'})`; + imageNotice = `1 foto optimizada (${isSquare ? '640x640' : '640x360'})\n 📐 _${focusLabel}_`; } const requestText = `🔔 *PUBLICACIÓN LISTA - SOLICITUD DE APROBACIÓN*\n\n` diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts index 066272b..c336d2b 100644 --- a/src/lib/gemini.ts +++ b/src/lib/gemini.ts @@ -16,6 +16,8 @@ export interface ProcessedPublication { fecha_vencimiento: string | null; selected_image_index?: number; image_selection_reason?: string; + crop_focus?: 'top' | 'center' | 'bottom' | 'contain'; + has_text_overlay?: boolean; } /** @@ -52,7 +54,9 @@ function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPub contenido: contenido, fecha_vencimiento: null, selected_image_index: mediaCount > 0 ? 0 : undefined, - image_selection_reason: mediaCount > 0 ? 'Foto destacada principal' : undefined + image_selection_reason: mediaCount > 0 ? 'Foto destacada principal' : undefined, + crop_focus: 'center', + has_text_overlay: false }; } @@ -90,6 +94,12 @@ export async function processWhatsAppMessage( 5. fecha_vencimiento: Si el texto indica fecha límite del evento o del aviso (Ej. "hasta el 30 de noviembre", o "el próximo sábado"), devuélvela en formato YYYY-MM-DD. Si no aplica o es una noticia general, pasa null. 6. selected_image_index: Si se proporcionaron múltiples imágenes candidatas (#1, #2, #3...), selecciona el índice base 0 de la imagen MÁS ADECUADA y de mayor calidad periodística/visual para ser la IMAGEN DESTACADA (la más nítida, bien compuesta, representativa del tema central y sin recortes desfavorables). Si solo hay 1 imagen, devuelve 0. Si no hay imágenes, pasa null. 7. image_selection_reason: Breve explicación (máximo 1 línea) del motivo por el cual esa imagen es la más adecuada para portada. + 8. crop_focus: Analiza el encuadre óptimo de la imagen seleccionada para evitar que se corten textos, titulares o rostros clave: + - "top": Si el texto principal, encabezado o rostros clave están en la parte superior. + - "center": Si la composición principal está equilibrada en el centro. + - "bottom": Si los elementos críticos están en la parte inferior. + - "contain": Si la imagen es un afiche, flyer vertical o infografía con texto de borde a borde donde cualquier recorte mutilaría información. + 9. has_text_overlay: Boolean. true si la imagen es un afiche/poster/flyer con texto integrado; false si es una fotografía estándar. `; // Prepara los contenidos @@ -131,7 +141,9 @@ export async function processWhatsAppMessage( contenido: { type: Type.STRING, description: "Contenido en HTML" }, fecha_vencimiento: { type: Type.STRING, description: "Fecha opcional YYYY-MM-DD", nullable: true }, selected_image_index: { type: Type.INTEGER, description: "Índice base 0 de la mejor imagen seleccionada", nullable: true }, - image_selection_reason: { type: Type.STRING, description: "Motivo de selección de imagen", nullable: true } + image_selection_reason: { type: Type.STRING, description: "Motivo de selección de imagen", nullable: true }, + crop_focus: { type: Type.STRING, enum: ["top", "center", "bottom", "contain"], description: "Punto de encuadre o gravedad óptimo sin cortar textos" }, + has_text_overlay: { type: Type.BOOLEAN, description: "Indica si contiene texto publicitario o afiche" } }, required: ["titulo", "tipo", "seo_descripcion", "contenido"] }