feat(ia): analisis de encuadre inteligente y deteccion de texto para evitar cortes en imagenes

This commit is contained in:
AI Bot 2026-08-24 16:32:31 -05:00
parent c8fd30e1c8
commit 1f8498e5a7
2 changed files with 32 additions and 8 deletions

View File

@ -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`

View File

@ -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"]
}