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 }); } }