// SKATEMAKER PRO — MODELBEAST client. Talks only to serve.py's /mb/ proxy; // the farm token never reaches the browser. Guest tier: max 4 active jobs. // Contract (mb_recon.py, verified 2026-07-16): submit {operator, asset_id?, params}, // poll status, then find your output by parent_job in the assets list. async function j(url, opts) { const r = await fetch(url, opts); const body = await r.json(); if (body.error) throw new Error(body.error); return body; } export async function listAssets() { const a = await j('/mb/assets'); return Array.isArray(a) ? a : a.items || []; } export const importAsset = (id, name) => j(`/mb/import?asset=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`); export const submitJob = (operator, params = {}, assetId = null) => j('/mb/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(assetId ? { operator, asset_id: assetId, params } : { operator, params }), }); export async function pollJob(id, onStatus, timeoutS = 600) { const t0 = Date.now(); for (;;) { await new Promise(res => setTimeout(res, 5000)); const st = (await j(`/mb/jobs/${id}`)).status; onStatus?.(`job ${id}: ${st} (${((Date.now() - t0) / 1000) | 0}s)`); if (st === 'done') return; if (st === 'error' || st === 'cancelled') throw new Error('job ended ' + st); if (Date.now() - t0 > timeoutS * 1000) throw new Error('job timed out'); } } async function outputOf(jobId, extRe) { const assets = await listAssets(); const mine = assets.filter(a => a.parent_job === jobId && extRe.test(a.name || a.filename || '')); return mine[0] || null; } // prompt -> image on the farm (flux_local, ~9s on the m3ultra) export async function generateImage(prompt, onStatus) { onStatus?.('submitting flux_local…'); const job = await submitJob('flux_local', { prompt }); await pollJob(job.id, onStatus); return outputOf(job.id, /\.(png|jpe?g|webp)$/i); } // MB image asset -> same image with background removed (for clean decals) export async function removeBg(assetId, onStatus) { onStatus?.('submitting bg_remove_local…'); const job = await submitJob('bg_remove_local', {}, assetId); await pollJob(job.id, onStatus); return outputOf(job.id, /\.(png|webp)$/i); } // MB image asset -> GLB (hunyuan3d_mlx). Slow — minutes, not seconds. export async function imageTo3D(assetId, onStatus) { onStatus?.('submitting hunyuan3d_mlx…'); const job = await submitJob('hunyuan3d_mlx', {}, assetId); await pollJob(job.id, onStatus, 1800); return outputOf(job.id, /\.glb$/i); }