export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Método no permitido' }); } try { const { prompt, model = 'black-forest-labs/FLUX.1-schnell', negative_prompt = '' } = req.body; if (!prompt) { return res.status(400).json({ error: 'El prompt es requerido.' }); } const hfToken = process.env.HF_TOKEN; const togetherKey = process.env.TOGETHER_API_KEY; let dataUrl = null; let modelUsed = model; const errors = []; // ─── 1. Together AI (directo) ────────────────────────────────────────── // Proveedor primario: FLUX.1-schnell-Free es 100% gratis en Together AI. // Registrarse en: https://api.together.ai/signup (sin tarjeta de crédito) // Agregar TOGETHER_API_KEY en Vercel Settings → Environment Variables if (togetherKey) { try { console.log('[Together] Intentando FLUX.1-schnell-Free...'); const togetherRes = await fetch('https://api.together.xyz/v1/images/generations', { method: 'POST', headers: { 'Authorization': `Bearer ${togetherKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'black-forest-labs/FLUX.1-schnell-Free', prompt: prompt, width: 1024, height: 1024, steps: 4, n: 1, response_format: 'b64_json' }), signal: AbortSignal.timeout(52000) }); if (togetherRes.ok) { const json = await togetherRes.json(); const b64 = json?.data?.[0]?.b64_json; if (b64) { dataUrl = `data:image/jpeg;base64,${b64}`; modelUsed = 'FLUX.1 Schnell (Together AI)'; console.log('[Together] ✓ Éxito'); } else { const msg = 'Sin b64_json en respuesta: ' + JSON.stringify(json).slice(0, 200); errors.push('[Together] ' + msg); console.warn('[Together]', msg); } } else { const errText = await togetherRes.text().catch(() => ''); const msg = `Status ${togetherRes.status}: ${errText.slice(0, 200)}`; errors.push('[Together] ' + msg); console.warn('[Together] ✗', msg); } } catch (e) { errors.push('[Together Error] ' + e.message); console.warn('[Together Error]', e.message); } } else { const msg = 'TOGETHER_API_KEY no configurado. Registrarse gratis en https://api.together.ai/signup'; errors.push('[Together] ' + msg); console.warn('[Together]', msg); } // ─── 2. Fal.ai vía HF Router ────────────────────────────────────────── // router.huggingface.co resuelve DNS correctamente desde Vercel (confirmado). if (!dataUrl && hfToken) { try { console.log('[Fal/HF] Intentando fal-ai/flux/schnell...'); const falRes = await fetch('https://router.huggingface.co/fal-ai/fal-ai/flux/schnell', { method: 'POST', headers: { 'Authorization': `Bearer ${hfToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: prompt, image_size: 'square_hd', num_inference_steps: 4, num_images: 1, enable_safety_checker: false }), signal: AbortSignal.timeout(52000) }); if (falRes.ok) { const json = await falRes.json(); const imgUrl = json?.images?.[0]?.url; if (imgUrl) { // Fal retorna URL, hay que descargar la imagen const imgRes = await fetch(imgUrl, { signal: AbortSignal.timeout(20000) }); if (imgRes.ok) { const ct = imgRes.headers.get('content-type') || 'image/jpeg'; const buf = await imgRes.arrayBuffer(); dataUrl = `data:${ct};base64,${Buffer.from(buf).toString('base64')}`; modelUsed = 'FLUX.1 Schnell (Fal.ai via HF)'; console.log('[Fal/HF] ✓ Éxito'); } } else if (json?.images?.[0]?.content) { // Fal alternativo: retorna base64 directo dataUrl = `data:image/jpeg;base64,${json.images[0].content}`; modelUsed = 'FLUX.1 Schnell (Fal.ai via HF)'; console.log('[Fal/HF] ✓ Éxito (b64)'); } else { const msg = 'Sin imagen en respuesta: ' + JSON.stringify(json).slice(0, 200); errors.push('[Fal/HF] ' + msg); console.warn('[Fal/HF]', msg); } } else { const errText = await falRes.text().catch(() => ''); const msg = `Status ${falRes.status}: ${errText.slice(0, 200)}`; errors.push('[Fal/HF] ' + msg); console.warn('[Fal/HF] ✗', msg); } } catch (e) { errors.push('[Fal/HF Error] ' + e.message); console.warn('[Fal/HF Error]', e.message); } } // ─── 3. Novita AI vía HF Router ─────────────────────────────────────── if (!dataUrl && hfToken) { try { console.log('[Novita/HF] Intentando FLUX.1-schnell...'); const novitaRes = await fetch('https://router.huggingface.co/novita/v1/images/generations', { method: 'POST', headers: { 'Authorization': `Bearer ${hfToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'black-forest-labs/FLUX.1-schnell', prompt: prompt, width: 1024, height: 1024, steps: 4, n: 1, response_format: 'b64_json' }), signal: AbortSignal.timeout(52000) }); if (novitaRes.ok) { const json = await novitaRes.json(); const b64 = json?.data?.[0]?.b64_json; if (b64) { dataUrl = `data:image/jpeg;base64,${b64}`; modelUsed = 'FLUX.1 Schnell (Novita via HF)'; console.log('[Novita/HF] ✓ Éxito'); } else { const msg = 'Sin b64_json: ' + JSON.stringify(json).slice(0, 200); errors.push('[Novita/HF] ' + msg); console.warn('[Novita/HF]', msg); } } else { const errText = await novitaRes.text().catch(() => ''); const msg = `Status ${novitaRes.status}: ${errText.slice(0, 200)}`; errors.push('[Novita/HF] ' + msg); console.warn('[Novita/HF] ✗', msg); } } catch (e) { errors.push('[Novita/HF Error] ' + e.message); console.warn('[Novita/HF Error]', e.message); } } // ─── 4. Pollinations AI (fallback gratuito) ──────────────────────────── if (!dataUrl) { const pollModels = ['turbo', 'flux-schnell']; for (const pollModel of pollModels) { try { console.log(`[Pollinations] Intentando model=${pollModel}...`); const seed = Math.floor(Math.random() * 100000); const encodedPrompt = encodeURIComponent(prompt); const pollUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=512&height=512&model=${pollModel}&nologo=true&seed=${seed}`; const pollRes = await fetch(pollUrl, { signal: AbortSignal.timeout(45000), redirect: 'follow' }); if (pollRes.ok) { const ct = pollRes.headers.get('content-type') || ''; if (ct.startsWith('image/')) { const buf = await pollRes.arrayBuffer(); dataUrl = `data:${ct};base64,${Buffer.from(buf).toString('base64')}`; modelUsed = `FLUX (Pollinations/${pollModel})`; console.log(`[Pollinations] ✓ Éxito con ${pollModel}`); break; } } else { const msg = `model=${pollModel} -> Status ${pollRes.status}`; errors.push('[Pollinations] ' + msg); console.warn('[Pollinations] ✗', msg); } } catch (e) { errors.push(`[Pollinations Error/${pollModel}] ` + e.message); console.warn(`[Pollinations Error/${pollModel}]`, e.message); } } } if (!dataUrl) { console.error('[2D] Todos los proveedores fallaron:', errors); const noTogether = !togetherKey ? ' | SOLUCIÓN: Agregar TOGETHER_API_KEY en Vercel (gratis en together.ai)' : ''; return res.status(500).json({ error: `No se pudo generar la imagen.${noTogether}`, debug: errors }); } return res.status(200).json({ success: true, image: dataUrl, imageUrl: dataUrl, model_used: modelUsed }); } catch (err) { console.error('[Vercel Serverless 2D Error]', err); return res.status(500).json({ error: err.message || 'Error interno.' }); } }