File size: 4,411 Bytes
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
 * vision.js — Análisis de Imágenes v1.1
 * FIX: notifica al usuario cuando todos los modelos fallan en vez de devolver null silencioso
 */

import { readConfig }     from './utils.js';
import { analyzeUserImage as visionAgentAnalyze } from './vision-agent.js';
import { sanitizeOutput } from './security.js';

const config = readConfig();

const VISION_MODELS = [
  'google/gemini-2.0-flash-001',
  'google/gemini-2.5-pro-preview',
  'meta-llama/llama-4-maverick:free',
];

export async function analyzeImage(imageUrls, prompt = 'Describe esta imagen en español de forma breve.') {
  const urls = Array.isArray(imageUrls) ? imageUrls : [imageUrls];

  // PRIMERA OPCIÓN: vision-agent (Gemini Vision directo — más rápido y sin gastar API de texto)
  if (urls.length > 0) {
    try {
      const firstUrl = typeof urls[0] === 'string' ? urls[0] : urls[0]?.url ?? urls[0]?.proxyURL ?? '';
      if (firstUrl.startsWith('http')) {
        const imgRes  = await fetch(firstUrl, { signal: AbortSignal.timeout(12000) });
        if (imgRes.ok) {
          const b64    = Buffer.from(await imgRes.arrayBuffer()).toString('base64');
          const result = await visionAgentAnalyze(b64, prompt);
          if (result && result.length > 15 && !result.includes('No se pudo')) {
            return result;
          }
        }
      }
    } catch { /* fallback al método original via OpenRouter */ }
  }

  const imageContent = urls.slice(0, 4).map(url => ({
    type: 'image_url', image_url: { url, detail: 'auto' }
  }));

  for (const model of VISION_MODELS) {
    try {
      const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
        method : 'POST',
        headers: {
          'Authorization': `Bearer ${config.ai.openrouter.apiKey}`,
          'Content-Type' : 'application/json',
          'HTTP-Referer' : 'https://tomatesmp.pw',
          'X-Title'      : 'Zelin',
        },
        body  : JSON.stringify({
          model,
          messages: [{ role: 'user', content: [
            { type: 'text', text: prompt },
            ...imageContent,
          ]}],
          max_tokens: 500,
        }),
        signal: AbortSignal.timeout(30000),
      });

      if (!res.ok) { console.warn(`[Vision] ${model}: ${res.status}`); continue; }
      const data    = await res.json();
      const content = data.choices?.[0]?.message?.content;
      if (!content) continue;
      console.log(`[Vision] ✅ ${model} (${urls.length} imagen${urls.length > 1 ? 'es' : ''})`);
      return sanitizeOutput(content);
    } catch (err) {
      console.warn(`[Vision] ${model} error:`, err.message);
    }
  }

  // FIX: antes devolvía null silencioso — ahora devuelve mensaje de error
  console.error('[Vision] Todos los modelos de visión fallaron');
  return 'no pude analizar la imagen ahora mismo, inténtalo de nuevo';
}

export function getImageAttachments(message) {
  const images = [];
  for (const att of message.attachments.values()) {
    const isImage = att.contentType?.startsWith('image/') || /\.(png|jpg|jpeg|gif|webp)$/i.test(att.name ?? '');
    if (isImage) images.push(att.proxyURL ?? att.url);
  }
  for (const embed of message.embeds ?? []) {
    if (embed.image?.url) images.push(embed.image.url);
  }
  return images;
}

export function getStickerUrls(message) {
  return [...(message.stickers?.values() ?? [])].map(s => s.url).filter(Boolean);
}

export function isImageRequest(content) {
  if (!content) return false;
  const l = content.toLowerCase();
  return l.includes('qué hay') || l.includes('que hay') || l.includes('analiza') ||
         l.includes('describe') || l.includes('qué dice') || l.includes('que dice') ||
         l.includes('lee') || l.includes('texto') || l.includes('imagen') ||
         l.includes('foto') || l.includes('screenshot') || l.includes('captura') ||
         l.includes('qué ves') || l.includes('que ves') || l.includes('ocr');
}

export function buildVisionPrompt(userMessage) {
  const l = userMessage.toLowerCase();
  if (l.includes('texto') || l.includes('dice') || l.includes('lee') || l.includes('ocr')) {
    return 'Extrae y transcribe todo el texto visible en esta imagen. Si no hay texto, descríbela brevemente.';
  }
  if (l.includes('describe') || l.includes('qué hay') || l.includes('qué ves')) {
    return 'Describe detalladamente qué hay en esta imagen en español.';
  }
  return userMessage;
}