export default async function handler(req, res) { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); if (req.method !== 'GET') { return res.status(405).json({ error: 'Método no permitido' }); } try { let jobId = req.query?.job_id; if (!jobId && req.url) { try { const parsedUrl = new URL(req.url, 'https://localhost'); jobId = parsedUrl.searchParams.get('job_id'); } catch (e) {} } if (!jobId) { return res.status(400).json({ error: 'El parámetro job_id es requerido.' }); } const hfToken = process.env.HF_TOKEN; if (jobId.startsWith('hf:')) { const parts = jobId.split(':'); const spaceHost = parts[1] || 'logicaltrue-trellis-2'; const sessionHash = parts[2]; const eventId = parts[3]; const decimationTarget = parseInt(parts[4]) || 300000; const textureSize = parseInt(parts[5]) || 1024; const spaceUrl = `https://${spaceHost}.hf.space`; // Cabecera obligatoria Accept: text/event-stream para endpoints SSE de Gradio v4 const sseHeaders = { 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', ...(hfToken ? { 'Authorization': `Bearer ${hfToken}` } : {}) }; const jsonHeaders = { 'Content-Type': 'application/json', ...(hfToken ? { 'Authorization': `Bearer ${hfToken}` } : {}) }; // ─── 1. Verificar estado del Paso 1 (/image_to_3d) ──────────────────── const step1Urls = [ `${spaceUrl}/gradio_api/call/image_to_3d/${eventId}`, `${spaceUrl}/call/image_to_3d/${eventId}`, `${spaceUrl}/gradio_api/queue/data?session_hash=${sessionHash}`, `${spaceUrl}/queue/data?session_hash=${sessionHash}` ]; let isStep1Complete = false; for (const statusUrl of step1Urls) { try { const qRes = await fetch(statusUrl, { headers: sseHeaders, signal: AbortSignal.timeout(9000) }).catch(() => null); if (qRes && qRes.ok) { const txt = await qRes.text().catch(() => ''); console.log(`[Paso 1 Stream SSE ${eventId}]`, txt.slice(0, 300)); // Validar si el evento complete o process_completed fue emitido con éxito if (txt.includes('event: complete') || txt.includes('process_completed') || txt.includes('event: generating')) { if (txt.includes('"success":false')) { continue; } isStep1Complete = true; break; } if (txt.includes('process_starts') || txt.includes('estimation') || txt.includes('event: heartbeat')) { return res.status(200).json({ status: 'processing', progress: 65, message: 'Inferencia 3D en progreso en la GPU A100 (Sparse & SLaT)...' }); } } } catch (e) {} } // Si la GPU aún está calculando el volumen 3D if (!isStep1Complete) { return res.status(200).json({ status: 'processing', progress: 60, message: 'Calculando volumen 3D en la GPU A100...' }); } // ─── 2. Paso 1 completado! Ejecutar Extracción GLB (/extract_glb) ───── console.log(`[Paso 1 completado] Iniciando Paso 2 (/extract_glb) decimation=${decimationTarget}...`); let extractEventId = null; try { const extractRes = await fetch(`${spaceUrl}/gradio_api/call/extract_glb`, { method: 'POST', headers: jsonHeaders, body: JSON.stringify({ data: [decimationTarget, textureSize], session_hash: sessionHash }), signal: AbortSignal.timeout(10000) }).catch(() => null); if (extractRes && extractRes.ok) { const extractJson = await extractRes.json().catch(() => ({})); extractEventId = extractJson.event_id; console.log(`[Paso 2 /extract_glb Iniciado] event_id: ${extractEventId}`); } } catch (e) {} // Consultar resultado de /extract_glb const extractCheckUrls = [ extractEventId ? `${spaceUrl}/gradio_api/call/extract_glb/${extractEventId}` : null, extractEventId ? `${spaceUrl}/call/extract_glb/${extractEventId}` : null, `${spaceUrl}/gradio_api/queue/data?session_hash=${sessionHash}`, `${spaceUrl}/queue/data?session_hash=${sessionHash}` ].filter(Boolean); for (const extUrl of extractCheckUrls) { try { const extRes = await fetch(extUrl, { headers: sseHeaders, signal: AbortSignal.timeout(9000) }).catch(() => null); if (extRes && extRes.ok) { const extTxt = await extRes.text().catch(() => ''); const glbMatch = extTxt.match(/"([^"]+\.glb)"/i); if (glbMatch && glbMatch[1]) { let rawGlb = glbMatch[1]; let fullGlbUrl = rawGlb; if (!rawGlb.startsWith('http')) { if (rawGlb.startsWith('/tmp/') || rawGlb.startsWith('tmp/')) { fullGlbUrl = `${spaceUrl}/file=${rawGlb.startsWith('/') ? '' : '/'}${rawGlb}`; } else if (rawGlb.startsWith('/file=') || rawGlb.startsWith('file=')) { fullGlbUrl = `${spaceUrl}${rawGlb.startsWith('/') ? '' : '/'}${rawGlb}`; } else { fullGlbUrl = `${spaceUrl}/file=${rawGlb}`; } } console.log(`[GLB EXTRACCION EXITOSA 100%] ${fullGlbUrl}`); return res.status(200).json({ status: 'completed', progress: 100, result: { gltfUrl: fullGlbUrl, glbUrl: fullGlbUrl, fbxUrl: fullGlbUrl, detectedCategory: 'objeto' } }); } } } catch (e) {} } return res.status(200).json({ status: 'processing', progress: 85, message: 'Extrayendo texturas PBR y generando archivo GLB...' }); } return res.status(200).json({ status: 'processing', progress: 50, message: 'Procesando modelo 3D en la nube...' }); } catch (err) { console.error('[Vercel Job-Status Error]', err); return res.status(500).json({ error: err.message || 'Error verificando el estado de la tarea.' }); } }