23dfactory / frontend /api /generate-3d.js
LogicalTrue
fix: castear la resolucion a String ('1024' o '512') para validar con el selector de Gradio
c37eb46
Raw
History Blame Contribute Delete
7.75 kB
export default async function handler(req, res) {
const startTime = Date.now();
const logTrace = [];
function addLog(stage, message, data = null) {
const entry = {
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - startTime,
stage: stage,
message: message,
data: data
};
logTrace.push(entry);
console.log(`[3D-DIAGNOSTIC] [${stage}] ${message}`, data ? JSON.stringify(data).slice(0, 300) : '');
}
if (req.method !== 'POST') {
return res.status(405).json({ error: 'M茅todo no permitido', logs: logTrace });
}
try {
const body = req.body || {};
const {
image,
seed = Math.floor(Math.random() * 100000),
resolution = '1024',
ss_guidance = 7.5,
ss_steps = 12,
slat_guidance = 3.0,
slat_steps = 12,
texture_size = 1024,
decimation_target = 300000
} = body;
const seedNum = parseInt(seed) || Math.floor(Math.random() * 100000);
const resolutionStr = String(resolution || '1024');
const ssGuidanceNum = parseFloat(ss_guidance) || 7.5;
const ssStepsNum = parseInt(ss_steps) || 12;
const slatGuidanceNum = parseFloat(slat_guidance) || 3.0;
const slatStepsNum = parseInt(slat_steps) || 12;
const textureSizeNum = parseInt(texture_size) || 1024;
const decimationNum = parseInt(decimation_target) || 300000;
const sessionHash = Math.random().toString(36).substring(2, 13) + Math.random().toString(36).substring(2, 13);
addLog('INIT', 'Iniciando Carga de Imagen y Generaci贸n Gradio 4 v3', {
hasImage: Boolean(image),
imageLength: image ? image.length : 0,
seed: seedNum,
resolution: resolutionStr,
sessionHash
});
if (!image) {
return res.status(400).json({ error: 'La imagen 2D es requerida para la generaci贸n 3D.', logs: logTrace });
}
const hfToken = process.env.HF_TOKEN;
const spaceUrl = 'https://logicaltrue-trellis-2.hf.space';
const headers = {
...(hfToken ? { 'Authorization': `Bearer ${hfToken}` } : {})
};
let remoteFilePath = null;
const base64Data = image.replace(/^data:image\/\w+;base64,/, '');
const imgBuffer = Buffer.from(base64Data, 'base64');
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
let formDataParts = [];
formDataParts.push(`--${boundary}\r\nContent-Disposition: form-data; name="files"; filename="input.png"\r\nContent-Type: image/png\r\n\r\n`);
const headerBuf = Buffer.from(formDataParts.join(''));
const footerBuf = Buffer.from(`\r\n--${boundary}--\r\n`);
const fullBody = Buffer.concat([headerBuf, imgBuffer, footerBuf]);
const uploadEndpoints = [
`${spaceUrl}/gradio_api/upload`,
`${spaceUrl}/upload`
];
for (const uUrl of uploadEndpoints) {
addLog('UPLOAD_TRY', `Subiendo imagen 2D a ${uUrl}...`);
try {
const uploadRes = await fetch(uUrl, {
method: 'POST',
headers: {
...headers,
'Content-Type': `multipart/form-data; boundary=${boundary}`
},
body: fullBody,
signal: AbortSignal.timeout(12000)
}).catch(e => ({ ok: false, statusText: e.message }));
if (uploadRes && uploadRes.ok) {
const uploadJson = await uploadRes.json().catch(() => []);
if (Array.isArray(uploadJson) && uploadJson.length > 0) {
remoteFilePath = uploadJson[0];
addLog('UPLOAD_SUCCESS', `Imagen cargada con 茅xito en ${uUrl}`, { remotePath: remoteFilePath });
break;
}
} else if (uploadRes) {
const errTxt = await uploadRes.text().catch(() => '');
addLog('UPLOAD_WARN', `Status ${uploadRes.status} en ${uUrl}`, { error: errTxt.slice(0, 200) });
}
} catch (e) {
addLog('UPLOAD_EXCEPT', `Excepci贸n en ${uUrl}`, { error: e.message });
}
}
const gradioImageObj = remoteFilePath ? {
path: remoteFilePath,
url: `${spaceUrl}/file=${remoteFilePath}`,
orig_name: 'input.png',
mime_type: 'image/png',
meta: { _type: 'gradio.FileData' }
} : (typeof image === 'string' && (image.startsWith('data:') || image.startsWith('http')) ? {
path: image,
url: image,
orig_name: 'input.png',
meta: { _type: 'gradio.FileData' }
} : image);
const gradioData3D = [
gradioImageObj,
seedNum,
resolutionStr,
ssGuidanceNum,
0.7,
ssStepsNum,
5.0,
slatGuidanceNum,
0.5,
slatStepsNum,
3.0,
1.0,
0.0,
12,
3.0
];
addLog('PASO1_SUBMIT', 'Enviando FileData verificado a /image_to_3d...', { remotePath: remoteFilePath });
let step1EventId = null;
const step1Targets = [
`${spaceUrl}/gradio_api/call/image_to_3d`,
`${spaceUrl}/call/image_to_3d`
];
for (const targetUrl of step1Targets) {
try {
const sRes = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify({
data: gradioData3D,
fn_index: 0,
session_hash: sessionHash
}),
signal: AbortSignal.timeout(12000)
}).catch(() => null);
if (sRes && sRes.ok) {
const sJson = await sRes.json().catch(() => ({}));
if (sJson.event_id) {
step1EventId = sJson.event_id;
addLog('PASO1_SUCCESS', 'Paso 1 (/image_to_3d) ejecut谩ndose en la GPU A100', { event_id: step1EventId });
break;
}
}
} catch (e) {
addLog('PASO1_ERROR', `Error llamando ${targetUrl}`, { error: e.message });
}
}
if (step1EventId) {
const jobId = `hf:logicaltrue-trellis-2:${sessionHash}:${step1EventId}:${decimationNum}:${textureSizeNum}`;
return res.status(202).json({
success: true,
job_id: jobId,
status: 'pending',
provider: 'Hugging Face Space (LogicalTrue/TRELLIS.2)',
credits: 999,
diagnostics: logTrace
});
}
addLog('FATAL_END', 'No se pudo iniciar /image_to_3d en Trellis 2.');
return res.status(500).json({
error: 'No se pudo iniciar el proceso 3D en Hugging Face Space.',
diagnostics: logTrace
});
} catch (err) {
addLog('FATAL_EXCEPT', 'Excepci贸n general', { error: err.message, stack: err.stack });
return res.status(500).json({
error: err.message || 'Error en la generaci贸n 3D.',
diagnostics: logTrace
});
}
}