"use client"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Loader2 } from "lucide-react"; // ---------- Preview + batch helpers ---------- const PREVIEW_MAX = 2048; // max long-edge for on-screen previews const BATCH_SIZES = [2048, 1536, 1280, 1024, 864, 720]; // adaptive preview long-edges const COOLDOWN_MS = 150; // tiny cooldown between requests to ease VRAM async function makePreview(blob: Blob, maxEdge = PREVIEW_MAX): Promise { const bitmap = await createImageBitmap(blob); const { width, height } = bitmap; const scale = Math.min(1, maxEdge / Math.max(width, height)); const outW = Math.max(1, Math.round(width * scale)); const outH = Math.max(1, Math.round(height * scale)); const canvas = document.createElement("canvas"); canvas.width = outW; canvas.height = outH; const ctx = canvas.getContext("2d")!; ctx.drawImage(bitmap, 0, 0, outW, outH); bitmap.close(); const outBlob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b!), "image/png") ); return outBlob; } function revoke(url: string | null | undefined) { if (!url) return; try { URL.revokeObjectURL(url); } catch {} } // ---------- Methods ---------- type Canonical = | "ormbg" | "u2net" | "basnet" | "deeplab" | "tracer" | "u2net_human_seg" | "isnet-general-use" | "isnet-anime" | "bria" | "inspyrenet"; const METHODS: { key: Canonical; label: string }[] = [ { key: "ormbg", label: "ORMBG" }, { key: "u2net", label: "U2NET" }, { key: "basnet", label: "BASNET" }, { key: "deeplab", label: "DEEPLAB" }, { key: "tracer", label: "TRACER-B7" }, { key: "u2net_human_seg", label: "U2NET (Human)" }, { key: "isnet-general-use", label: "ISNET (General)" }, { key: "isnet-anime", label: "ISNET (Anime)" }, { key: "bria", label: "BRIA RMBG1.4" }, { key: "inspyrenet", label: "INSPYRENET" }, ]; const DEFAULT_CONCURRENCY = 2; type Status = "idle" | "pending" | "ok" | "error"; type ResultMap = { [K in Canonical]?: { fullBlob: Blob; previewUrl: string; bytes: number; ms: number; }; }; export default function BackgroundRemoverPage() { // ---------- State ---------- const [file, setFile] = useState(null); const [sourceUrl, setSourceUrl] = useState(null); const [natural, setNatural] = useState<{ w: number; h: number } | null>(null); const [status, setStatus] = useState>( () => Object.fromEntries(METHODS.map((m) => [m.key, "idle"])) as Record ); const [results, setResults] = useState({}); const resultsRef = useRef({}); useEffect(() => { resultsRef.current = results; }, [results]); const [active, setActive] = useState(null); const [reveal, setReveal] = useState(50); const [gpuSafe, setGpuSafe] = useState(true); const frameRef = useRef(null); const draggingRef = useRef(false); const batchBlobCache = useRef>(new Map()); useEffect(() => { return () => { revoke(sourceUrl); Object.values(resultsRef.current).forEach((v) => revoke(v?.previewUrl)); }; }, [sourceUrl]); // ---------- Styles ---------- const styles = ( ); // ---------- File pick ---------- const onPick = useCallback( async (f: File | null) => { revoke(sourceUrl); Object.values(resultsRef.current).forEach((v) => revoke(v?.previewUrl)); batchBlobCache.current.clear(); setFile(f); setResults({}); setActive(null); setReveal(50); setStatus(Object.fromEntries(METHODS.map((m) => [m.key, "idle"])) as any); if (!f) { setSourceUrl(null); setNatural(null); return; } try { const bmp = await createImageBitmap(f); setNatural({ w: bmp.width, h: bmp.height }); bmp.close(); } catch {} const previewBlob = await makePreview(f, PREVIEW_MAX); const previewUrl = URL.createObjectURL(previewBlob); setSourceUrl(previewUrl); }, [sourceUrl] ); // Get or create a cached resized blob for batch preview runs const getBatchBlob = useCallback( async (longEdge: number): Promise => { const cache = batchBlobCache.current; if (cache.has(longEdge)) return cache.get(longEdge)!; if (!file) throw new Error("No file selected"); const b = await makePreview(file, longEdge); cache.set(longEdge, b); return b; }, [file] ); // ---------- Batch run (adaptive) ---------- const startAll = useCallback(async () => { if (!file) return; setResults({}); setStatus((prev) => { const next = { ...prev }; METHODS.forEach((m) => (next[m.key] = "pending")); return next; }); const runOne = async (key: Canonical) => { // When GPU-safe is on, try progressively smaller long-edge previews. const sizes = gpuSafe ? BATCH_SIZES : [Math.max(natural?.w || 0, natural?.h || 0) || 4096]; let lastErr: string | null = null; const t0 = performance.now(); for (const size of sizes) { try { const blobToSend = gpuSafe ? await getBatchBlob(size) : file!; const fd = new FormData(); fd.append("file", blobToSend); fd.append("method", key); // CHANGED: call local proxy instead of a hardcoded service const res = await fetch("/api/bgbye/process", { method: "POST", body: fd }); if (!res.ok) { const txt = await res.text().catch(() => ""); const retryable = /out of memory|onnxruntime|cuda|allocate|500/i.test(txt); if (gpuSafe && retryable) { lastErr = txt || `HTTP ${res.status}`; continue; // try next smaller size } throw new Error(txt || `HTTP ${res.status}`); } const outBlob = await res.blob(); const ms = performance.now() - t0; const previewBlob = await makePreview(outBlob); const previewUrl = URL.createObjectURL(previewBlob); setResults((r) => ({ ...r, [key]: { fullBlob: outBlob, previewUrl, bytes: outBlob.size, ms }, })); setStatus((s) => ({ ...s, [key]: "ok" })); await new Promise((r) => setTimeout(r, COOLDOWN_MS)); // tiny cooldown return; } catch (e: any) { lastErr = e?.message || String(e); if (!gpuSafe) break; // not in adaptive mode, bail after first failure } } setStatus((s) => ({ ...s, [key]: "error" })); // (Optional) console.debug("Last error for", key, lastErr); }; const concurrency = gpuSafe ? 1 : DEFAULT_CONCURRENCY; const queue = [...METHODS.map((m) => m.key)]; let inFlight: Promise[] = []; const launch = () => { while (inFlight.length < concurrency && queue.length) { const key = queue.shift()!; const p = runOne(key).finally(() => { inFlight = inFlight.filter((q) => q !== p); }); inFlight.push(p); } }; launch(); while (inFlight.length) { await Promise.race(inFlight); launch(); } setActive((prev) => { if (prev) return prev; for (const m of METHODS) if ((resultsRef.current as any)[m.key]) return m.key; return METHODS[0]?.key ?? null; }); }, [file, gpuSafe, natural, getBatchBlob]); // ---------- Upload & slider handlers ---------- const onDrop = (e: React.DragEvent) => { e.preventDefault(); const f = e.dataTransfer.files?.[0]; if (f) onPick(f); }; const onSelect = (e: React.ChangeEvent) => { const f = e.target.files?.[0] ?? null; onPick(f); }; const aspect = useMemo(() => (!natural ? 16 / 9 : natural.w / natural.h || 16 / 9), [natural]); const updateByClientX = useCallback((clientX: number) => { const el = frameRef.current; if (!el) return; const rect = el.getBoundingClientRect(); const pct = ((clientX - rect.left) / rect.width) * 100; setReveal(Math.min(100, Math.max(0, pct))); }, []); const onMouseDown = (e: React.MouseEvent) => { draggingRef.current = true; updateByClientX(e.clientX); }; const onMouseMove = (e: React.MouseEvent) => { if (!draggingRef.current) return; updateByClientX(e.clientX); }; const onMouseUp = () => (draggingRef.current = false); const onTouchStart = (e: React.TouchEvent) => { draggingRef.current = true; updateByClientX(e.touches[0].clientX); }; const onTouchMove = (e: React.TouchEvent) => { if (!draggingRef.current) return; e.preventDefault(); // keep page from horizontal panning while sliding updateByClientX(e.touches[0].clientX); }; const onTouchEnd = () => (draggingRef.current = false); // ---------- Active result & actions ---------- const activeResult = active ? results[active] : undefined; const canDownload = Boolean(active && activeResult?.fullBlob); const download = () => { if (!active || !activeResult) return; const a = document.createElement("a"); const base = file?.name?.replace(/\.[^.]+$/, "") || "image"; const fullUrl = URL.createObjectURL(activeResult.fullBlob); a.href = fullUrl; setTimeout(() => revoke(fullUrl), 5000); a.download = `${base}_${active}.png`; document.body.appendChild(a); a.click(); a.remove(); }; // Re-run the selected method on the ORIGINAL file for full-resolution output const renderFullRes = useCallback(async () => { if (!file || !active) return; setStatus((s) => ({ ...s, [active]: "pending" })); const t0 = performance.now(); try { const fd = new FormData(); fd.append("file", file); fd.append("method", active); // CHANGED: call local proxy instead of a hardcoded service const res = await fetch("/api/bgbye/process", { method: "POST", body: fd }); if (!res.ok) throw new Error(await res.text()); const outBlob = await res.blob(); const ms = performance.now() - t0; const previewBlob = await makePreview(outBlob); const previewUrl = URL.createObjectURL(previewBlob); const prev = resultsRef.current[active]; if (prev) revoke(prev.previewUrl); setResults((r) => ({ ...r, [active]: { fullBlob: outBlob, previewUrl, bytes: outBlob.size, ms } })); setStatus((s) => ({ ...s, [active]: "ok" })); } catch { setStatus((s) => ({ ...s, [active]: "error" })); } }, [file, active]); const doneCount = useMemo(() => METHODS.filter((m) => status[m.key] === "ok").length, [status]); const pendingCount = useMemo(() => METHODS.filter((m) => status[m.key] === "pending").length, [status]); function StatusDot({ s }: { s: Status }) { const cls = s === "ok" ? "bg-emerald-500" : s === "pending" ? "bg-amber-400 animate-pulse" : s === "error" ? "bg-rose-500" : "bg-zinc-600"; return ; } // ---------- Render ---------- return (
{styles}
{/* Header row: title left, back button right */}

Background Remover

Back to main
{/* Source filename */}
Source:{" "} {file?.name ?? — none —}
{/* Preview frame (border removed per your note) */}
e.preventDefault()} onDrop={onDrop} onMouseDown={onMouseDown} onMouseMove={onMouseMove} onMouseLeave={onMouseUp} onMouseUp={onMouseUp} onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd} > {/* Drop hint */} {!sourceUrl && ( )} {/* Before/After */} {sourceUrl && ( <> {/* LEFT (BEFORE) */} Source {/* RIGHT (AFTER) */} {activeResult ? ( Result ) : status[active as Canonical] === "pending" ? (
) : null} {/* Divider & Thumb */}
)}
{/* Method options – EVEN GRID under the preview */}
{METHODS.map(({ key, label }) => ( ))}
{/* Actions & global status */}
{/* GPU-safe toggle */}
{file ? ( pendingCount > 0 ? ( Processing… {doneCount}/{METHODS.length} finished ) : doneCount > 0 ? ( Done: {doneCount} methods succeeded ) : ( Ready. Click Run all methods ) ) : ( Drop an image to begin )}
{/* Right-side controls */}
setReveal(parseInt(e.target.value, 10))} className="w-full sm:w-56" title="Slide to compare before/after" />
); }