Spaces:
Build error
Build error
| 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.' }); | |
| } | |
| } | |