// Processing + Result modals. // Talks to /gradio_api/* directly via fetch — bypasses @gradio/client (which // hardcodes /gradio/config on hf.space URLs and 404s against gradio.Server). // Auth comes from the HF OAuth session cookie set after /login/huggingface; // same-origin fetches forward it automatically with credentials: 'include'. // Client-side resize: prevents huge phone photos from OOMing ZeroGPU on decode. // Server still resizes to 1024 longest edge; this is an upstream safety net. async function resizeImageFile(file, maxEdge = 2048, quality = 0.92) { if (!/^image\//.test(file.type)) return file; const bitmap = await createImageBitmap(file); const { width: w, height: h } = bitmap; if (Math.max(w, h) <= maxEdge) { bitmap.close?.(); return file; } const scale = maxEdge / Math.max(w, h); const tw = Math.round(w * scale); const th = Math.round(h * scale); const canvas = document.createElement('canvas'); canvas.width = tw; canvas.height = th; canvas.getContext('2d').drawImage(bitmap, 0, 0, tw, th); bitmap.close?.(); const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', quality)); return new File([blob], (file.name || 'upload').replace(/\.[^.]+$/, '') + '.jpg', { type: 'image/jpeg' }); } // Generate a URL-safe session id. Used to key the server-side result store // so the frontend can recover after a tab-away/screen-lock. function newSessionId() { if (window.crypto && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, ''); return Array.from(crypto.getRandomValues(new Uint8Array(16))) .map(b => b.toString(16).padStart(2, '0')).join(''); } function ProcessingModal({ open, file, onDone, onError, onClose }) { const [progress, setProgress] = React.useState(0); const [step, setStep] = React.useState(0); const [imageUrl, setImageUrl] = React.useState(null); const cancelRef = React.useRef({ cancelled: false }); const steps = [ { t: 'Reading the brief', s: 'STEP 1' }, { t: 'Welding the joints', s: 'STEP 2' }, { t: 'Casting in plastic', s: 'STEP 3' }, { t: 'Out of the oven', s: 'STEP 4' }, { t: 'Painting final details', s: 'STEP 5' }, ]; React.useEffect(() => { if (!open || !file) return; cancelRef.current = { cancelled: false }; setProgress(0); setStep(0); const url = URL.createObjectURL(file); setImageUrl(url); // Stamp the URL with a session id so we can recover the result after // a tab-away. Cleared by the App component on modal close. const sessionId = newSessionId(); const params = new URLSearchParams(window.location.search); params.set('session', sessionId); window.history.replaceState(null, '', window.location.pathname + '?' + params.toString()); let p = 0; const id = setInterval(() => { if (cancelRef.current.cancelled) { clearInterval(id); return; } p += 0.6 + Math.random() * 1.2; if (p > 92) p = 92; setProgress(p); setStep(Math.min(4, Math.floor((p / 100) * 5))); }, 120); (async () => { try { const upload = await resizeImageFile(file, 2048, 0.92); // Use @gradio/client (loaded via CDN in index.html). It does the // ZeroGPU handshake — sends `supports-zerogpu-headers: 1` and exchanges // the iframe's __sign for a fresh proxy token. Manual fetch skips this // and the scheduler returns "Expired ZeroGPU proxy token". let tries = 0; while ((!window.__gradio || !window.__gradio.Client) && tries < 100) { await new Promise(r => setTimeout(r, 50)); tries++; } if (!window.__gradio || !window.__gradio.Client) { throw new Error('Gradio client failed to load from CDN'); } const { Client, handle_file } = window.__gradio; const client = await Client.connect(window.location.origin); const result = await client.predict('/action', { image_path: handle_file(upload), session_id: sessionId, }); if (cancelRef.current.cancelled) return; clearInterval(id); setProgress(100); setStep(4); const out = result?.data?.[0]; const outUrl = typeof out === 'string' ? out : (out?.url || out?.path); setTimeout(() => { if (!cancelRef.current.cancelled) onDone(outUrl); }, 400); return; } catch (err) { clearInterval(id); if (!cancelRef.current.cancelled) { console.error('Inference failed:', err); onError && onError(err); } } })(); return () => { cancelRef.current.cancelled = true; clearInterval(id); URL.revokeObjectURL(url); }; }, [open, file]); if (!open) return null; return (
ACTION · PROCESSING · SESSION {Math.floor(Math.random() * 9999).toString(16).toUpperCase().padStart(4, '0')} CLOSE ×
{imageUrl && preview}
── MOULDING

{progress < 100 ? 'CASTING' : 'COMPLETING'}

ACT/BF16 · 1:6{Math.floor(progress)}%
QWEN-EDIT-2511
{steps.map((s, i) => (
{s.t} {s.s}
))}
); } function ResultModal({ open, beforeUrl, afterUrl, onClose }) { const [split, setSplit] = React.useState(50); const [aspect, setAspect] = React.useState(null); // Before image rescaled to the after image's exact dimensions for // pixel-perfect slider alignment. Display only — doesn't affect download. const [alignedBeforeUrl, setAlignedBeforeUrl] = React.useState(null); const compareRef = React.useRef(); // Read natural dimensions from the result image so the compare frame // matches its aspect ratio — avoids the 16:9 cover-crop on portraits. React.useEffect(() => { if (!afterUrl) { setAspect(null); return; } const img = new Image(); img.onload = () => { if (img.naturalWidth && img.naturalHeight) { setAspect(img.naturalWidth / img.naturalHeight); } }; img.src = afterUrl; }, [afterUrl]); // Rescale the before image (in a canvas) to match the after image's exact // dimensions. Pipeline outputs are at native ~1024² and don't match small // uploads pixel-for-pixel — without this, the slider drag would shimmer // on aspect-ratio differences from mult-of-32 rounding. React.useEffect(() => { if (!beforeUrl || !afterUrl) { setAlignedBeforeUrl(null); return; } let cancelled = false; let createdUrl = null; const before = new Image(); const after = new Image(); let beforeReady = false, afterReady = false; const tryDraw = () => { if (cancelled || !beforeReady || !afterReady) return; const w = after.naturalWidth, h = after.naturalHeight; if (!w || !h) return; const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(before, 0, 0, w, h); canvas.toBlob((blob) => { if (cancelled || !blob) return; createdUrl = URL.createObjectURL(blob); setAlignedBeforeUrl(createdUrl); }, 'image/png'); }; before.onload = () => { beforeReady = true; tryDraw(); }; after.onload = () => { afterReady = true; tryDraw(); }; before.src = beforeUrl; after.src = afterUrl; return () => { cancelled = true; if (createdUrl) URL.revokeObjectURL(createdUrl); }; }, [beforeUrl, afterUrl]); const onMove = (clientX) => { const el = compareRef.current; if (!el) return; const r = el.getBoundingClientRect(); const pct = Math.max(0, Math.min(100, ((clientX - r.left) / r.width) * 100)); setSplit(pct); }; const startDrag = (e) => { const move = (ev) => onMove(ev.touches ? ev.touches[0].clientX : ev.clientX); const up = () => { window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); window.removeEventListener('touchmove', move); window.removeEventListener('touchend', up); }; window.addEventListener('mousemove', move); window.addEventListener('mouseup', up); window.addEventListener('touchmove', move, { passive: true }); window.addEventListener('touchend', up); e.preventDefault(); }; const onDownload = async () => { if (!afterUrl) return; try { const res = await fetch(afterUrl); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'systms_action.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (e) { console.error('Download failed:', e); } }; if (!open) return null; return (
ACTION · RESULT · QWEN-EDIT-2511 CLOSE ×
{ onMove(e.clientX); startDrag(e); }} >
before
after
SUBJECT · SOURCE
FIGURE · MINT
MINT IN BOX
ACTION POSE
QWEN-EDIT-2511
); } Object.assign(window, { ProcessingModal, ResultModal });