23dfactory / api /generate-2d.js
LogicalTrue
feat: Together AI Primary
481c61f
Raw
History Blame Contribute Delete
11.1 kB
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.' });
}
}