diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts
index e45a014..066272b 100644
--- a/src/lib/gemini.ts
+++ b/src/lib/gemini.ts
@@ -18,15 +18,49 @@ export interface ProcessedPublication {
image_selection_reason?: string;
}
+/**
+ * Fallback parser in case Gemini API is rate-limited (429) or unavailable
+ */
+function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPublication {
+ const clean = (text || '').trim();
+ const lines = clean.split('\n').map(l => l.trim()).filter(Boolean);
+
+ let rawTitle = lines[0] || 'Nueva Publicación';
+ rawTitle = rawTitle.replace(/^[#*_\-•]+\s*/, '').replace(/[*_#]/g, '').trim();
+ if (rawTitle.length > 70) {
+ rawTitle = rawTitle.substring(0, 67) + '...';
+ }
+
+ const lower = clean.toLowerCase();
+ let tipo: 'noticia' | 'evento' | 'aviso' = 'noticia';
+ if (lower.includes('evento') || lower.includes('invitación') || lower.includes('invitacion') || lower.includes('jornada') || lower.includes('taller') || lower.includes('festival') || lower.includes('feria') || lower.includes('convocatoria')) {
+ tipo = 'evento';
+ } else if (lower.includes('se vende') || lower.includes('se arrienda') || lower.includes('clasificado') || lower.includes('empleo') || lower.includes('vacante') || lower.includes('servicio')) {
+ tipo = 'aviso';
+ }
+
+ const seo = clean.replace(/\n+/g, ' ').substring(0, 150) || rawTitle;
+
+ // Format content as clean HTML paragraphs
+ const paragraphs = lines.map(l => `
${l}
`).join('');
+ const contenido = paragraphs || `${clean || rawTitle}
`;
+
+ return {
+ titulo: rawTitle,
+ tipo: tipo,
+ seo_descripcion: seo,
+ contenido: contenido,
+ fecha_vencimiento: null,
+ selected_image_index: mediaCount > 0 ? 0 : undefined,
+ image_selection_reason: mediaCount > 0 ? 'Foto destacada principal' : undefined
+ };
+}
+
export async function processWhatsAppMessage(
text: string,
mediaParam?: string | MediaInput[] | MediaInput,
mimeTypeParam?: string
): Promise {
- if (!process.env.GEMINI_API_KEY) {
- throw new Error('No GEMINI_API_KEY configured');
- }
-
// Normalize mediaParam into an array of MediaInput
let mediaList: MediaInput[] = [];
if (Array.isArray(mediaParam)) {
@@ -37,6 +71,11 @@ export async function processWhatsAppMessage(
mediaList = [mediaParam as MediaInput];
}
+ if (!process.env.GEMINI_API_KEY) {
+ console.warn('[Gemini] Sin GEMINI_API_KEY configurada, usando parser de respaldo');
+ return fallbackRuleBasedParser(text, mediaList.length);
+ }
+
const systemInstruction = `
Eres un asistente editorial experto y editor gráfico de Almeira Web.
Analiza los mensajes y recursos multimedia recibidos desde WhatsApp para estructurarlos como publicaciones del portal.
@@ -68,32 +107,47 @@ export async function processWhatsAppMessage(
});
});
- const response = await ai.models.generateContent({
- model: 'gemini-3.6-flash',
- contents: contents,
- config: {
- systemInstruction: systemInstruction,
- responseMimeType: "application/json",
- responseSchema: {
- type: Type.OBJECT,
- properties: {
- titulo: { type: Type.STRING },
- tipo: { type: Type.STRING, enum: ["noticia", "evento", "aviso"] },
- seo_descripcion: { type: Type.STRING, description: "Máximo 150 caracteres" },
- 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 }
- },
- required: ["titulo", "tipo", "seo_descripcion", "contenido"]
- }
- }
- });
+ const candidateModels = [
+ 'gemini-2.5-flash',
+ 'gemini-2.0-flash',
+ 'gemini-1.5-flash',
+ 'gemini-2.0-flash-lite'
+ ];
- if (!response.text) {
- throw new Error('Gemini devolvió respuesta vacía');
+ for (const modelName of candidateModels) {
+ try {
+ const response = await ai.models.generateContent({
+ model: modelName,
+ contents: contents,
+ config: {
+ systemInstruction: systemInstruction,
+ responseMimeType: "application/json",
+ responseSchema: {
+ type: Type.OBJECT,
+ properties: {
+ titulo: { type: Type.STRING },
+ tipo: { type: Type.STRING, enum: ["noticia", "evento", "aviso"] },
+ seo_descripcion: { type: Type.STRING, description: "Máximo 150 caracteres" },
+ 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 }
+ },
+ required: ["titulo", "tipo", "seo_descripcion", "contenido"]
+ }
+ }
+ });
+
+ if (response.text) {
+ const result = JSON.parse(response.text) as ProcessedPublication;
+ return result;
+ }
+ } catch (err: any) {
+ console.warn(`[Gemini] Error con modelo ${modelName}:`, err?.message);
+ }
}
- const result = JSON.parse(response.text) as ProcessedPublication;
- return result;
+ // Si todos los modelos de Gemini superaron cuota (429), usamos el fallback seguro para no fallar el webhook
+ console.warn('[Gemini] Modelos de IA agotados por cuota, activando fallback editorial inteligente');
+ return fallbackRuleBasedParser(text, mediaList.length);
}