""" HDRemover.com — Complete SaaS Background Removal HuggingFace Space | Gradio 4.x """ import os, io, time, json, logging, random, string, threading from datetime import datetime, timezone, timedelta from collections import deque import numpy as np from PIL import Image logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s") logger = logging.getLogger("hdremover") def utcnow(): return datetime.now(timezone.utc) # ── Config ───────────────────────────────────────────────────────────────────── HF_TOKEN = os.environ.get("HF_TOKEN", "") MASTER_KEY = os.environ.get("MASTER_API_KEY", "hdremover-master-key-change-me") ADMIN_PASS = os.environ.get("ADMIN_PASSWORD", "hdremover-admin-2026") HF_REPO_ID = os.environ.get("HF_REPO_ID", "hdremover/background-remover") HF_DB_REPO = os.environ.get("HF_DB_REPO", "hdremover/background-remover-db") DB_FILE = "api_keys.json" # ── HF Login ─────────────────────────────────────────────────────────────────── if HF_TOKEN: try: from huggingface_hub import login login(token=HF_TOKEN, add_to_git_credential=False) logger.info("✅ HF login OK") except Exception as e: logger.warning(f"HF login: {e}") # ── Dataset Persistence ──────────────────────────────────────────────────────── _flush_counter = 0 _last_flush_ts = 0.0 FLUSH_EVERY_N = 50 FLUSH_EVERY_S = 300 def load_keys_from_dataset() -> dict: if not HF_TOKEN: return {} try: from huggingface_hub import hf_hub_download path = hf_hub_download(repo_id=HF_DB_REPO, filename=DB_FILE, repo_type="dataset", token=HF_TOKEN, force_download=True) with open(path) as f: data = json.load(f) logger.info(f"✅ Dataset loaded — {len(data)} keys") return data except Exception as e: logger.warning(f"Dataset load failed: {e}") return {} def save_keys_to_dataset(force=False) -> bool: global _flush_counter, _last_flush_ts if not HF_TOKEN: return False now = utcnow().timestamp() if not force: if _flush_counter < FLUSH_EVERY_N and (now - _last_flush_ts) < FLUSH_EVERY_S: return False try: from huggingface_hub import upload_file safe = {} for k, v in API_KEYS.items(): entry = dict(v) if "minute_window" in entry: entry["minute_window"] = list(entry["minute_window"]) safe[k] = entry content = json.dumps(safe, indent=2, default=str).encode("utf-8") upload_file(path_or_fileobj=io.BytesIO(content), path_in_repo=DB_FILE, repo_id=HF_DB_REPO, repo_type="dataset", token=HF_TOKEN, commit_message=f"flush {len(API_KEYS)} keys") _flush_counter = 0; _last_flush_ts = now logger.info(f"✅ Dataset saved — {len(API_KEYS)} keys") return True except Exception as e: logger.error(f"Dataset save failed: {e}") return False def _async_save(force=False): threading.Thread(target=save_keys_to_dataset, args=(force,), daemon=True).start() # ── API Keys ─────────────────────────────────────────────────────────────────── API_KEYS: dict = {} _from_dataset = load_keys_from_dataset() if _from_dataset: API_KEYS = _from_dataset for v in API_KEYS.values(): if "minute_window" in v: v["minute_window"] = deque(v["minute_window"]) logger.info(f"✅ Loaded {len(API_KEYS)} keys from dataset") else: _raw = os.environ.get("API_KEYS_JSON", "") if _raw: try: API_KEYS = json.loads(_raw) logger.info(f"✅ Loaded {len(API_KEYS)} keys from HF Secret") _async_save(force=True) except Exception as e: logger.warning(f"API_KEYS_JSON error: {e}") API_KEYS.setdefault(MASTER_KEY, { "plan": "master", "calls_today": 0, "reset_at": (utcnow() + timedelta(days=1)).timestamp(), "owner": "HDRemover.com", "created_at": utcnow().isoformat() }) PLAN_LIMITS = { "free": {"daily": 10, "per_minute": 2, "models": ["fast"]}, "starter": {"daily": 100, "per_minute": 10, "models": ["fast","quality","hair"]}, "pro": {"daily": 500, "per_minute": 30, "models": ["fast","quality","best","hair"]}, "master": {"daily": 999999, "per_minute": 999999, "models": ["fast","quality","best","hair"]}, } PLAN_DEFAULT_EXPIRY = {"free": 30, "starter": 365, "pro": 365, "master": None} def push_keys_to_hf_secret() -> tuple: _async_save(force=True) return True, "✅ **Saved!** Active immediately." def gen_key(plan: str) -> str: chars = string.ascii_lowercase + string.digits r = lambda n: ''.join(random.choices(chars, k=n)) return f"hdremover-{plan[:2]}-{r(8)}-{r(8)}" # ── Constants ────────────────────────────────────────────────────────────────── MAX_IMAGE_BYTES = 10 * 1024 * 1024 MAX_IMAGE_PX = 4000 # ── Model Cache ──────────────────────────────────────────────────────────────── _cache: dict = {} _keys_lock = threading.Lock() _failed_attempts: dict = {} _fail_lock = threading.Lock() MAX_FAILS = 10; FAIL_WINDOW = 300 def _check_brute_force(ip): now = utcnow().timestamp() with _fail_lock: a = _failed_attempts.setdefault(ip, deque()) while a and a[0] < now - FAIL_WINDOW: a.popleft() return len(a) >= MAX_FAILS def _record_fail(ip): with _fail_lock: _failed_attempts.setdefault(ip, deque()).append(utcnow().timestamp()) def load_u2net(): if "u2net" in _cache: return _cache["u2net"] try: from rembg import new_session _cache["u2net"] = new_session("u2net") logger.info("✅ U2-Net ready"); return _cache["u2net"] except Exception as e: logger.error(f"U2-Net: {e}"); return None def load_birefnet(): if "birefnet" in _cache: return _cache["birefnet"] try: from transformers import AutoModelForImageSegmentation from torchvision import transforms m = AutoModelForImageSegmentation.from_pretrained("ZhengPeng7/BiRefNet_lite", trust_remote_code=True) m = m.float(); m.eval() _cache["birefnet"] = m _cache["birefnet_tf"] = transforms.Compose([ transforms.Resize((1024,1024)), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]) logger.info("✅ BiRefNet-lite ready"); return m except Exception as e: logger.error(f"BiRefNet: {e}"); return None import warnings as _warnings def load_ben2(): if "ben2" in _cache: return _cache["ben2"] try: from ben2 import BEN_Base with _warnings.catch_warnings(): _warnings.filterwarnings("ignore", message=".*cuda.*CUDA is not available.*") m = BEN_Base.from_pretrained("PramaLLC/BEN2"); m.eval() _cache["ben2"] = m logger.info("✅ BEN2 ready"); return m except Exception as e: logger.error(f"BEN2: {e}"); return None def load_rmbg(): if "rmbg" in _cache: return _cache["rmbg"] try: from transformers import AutoModelForImageSegmentation from torchvision import transforms m = AutoModelForImageSegmentation.from_pretrained( "briaai/RMBG-2.0", trust_remote_code=True, token=HF_TOKEN or None) m = m.float(); m.eval() _cache["rmbg"] = m _cache["rmbg_tf"] = transforms.Compose([ transforms.Resize((1024,1024)), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]) logger.info("✅ RMBG-2.0 ready"); return m except Exception as e: logger.error(f"RMBG: {e}"); return None def _seg(key, img): import torch m = _cache[key]; tf = _cache[key+"_tf"]; orig = img.size inp = tf(img.convert("RGB")).unsqueeze(0).float() with torch.no_grad(): out = m(inp) pred = (out[-1] if isinstance(out,(list,tuple)) else out).sigmoid() mask = Image.fromarray((pred[0].squeeze().cpu().numpy()*255).astype(np.uint8)).resize(orig, Image.LANCZOS) r = img.convert("RGBA"); r.putalpha(mask); return r def infer_fast(img): from rembg import remove if not load_u2net(): raise RuntimeError("U2-Net not available") buf = io.BytesIO(); img.save(buf,"PNG"); buf.seek(0) return Image.open(io.BytesIO(remove(buf.read(), session=_cache["u2net"]))).convert("RGBA") def infer_quality(img): if not load_birefnet(): raise RuntimeError("BiRefNet not available") return _seg("birefnet", img) def infer_best(img): if not load_rmbg(): raise RuntimeError("RMBG-2.0 not available") return _seg("rmbg", img) def infer_hair(img): if not load_ben2(): raise RuntimeError("BEN2 not available") import torch _cache["ben2"].to(torch.device("cpu")) return _cache["ben2"].inference(img.convert("RGB"), refine_foreground=True) MODEL_FN = {"fast": infer_fast, "quality": infer_quality, "best": infer_best, "hair": infer_hair} MODEL_INFO = { "fast": {"name":"U2-Net", "desc":"Fastest (~1-5s)"}, "quality": {"name":"BiRefNet-lite", "desc":"High quality (~5-8s)"}, "best": {"name":"BRIA RMBG-2.0", "desc":"Best quality (~20s)"}, "hair": {"name":"BEN2", "desc":"Best for hair (~10-15s)"}, } # ── Auth ─────────────────────────────────────────────────────────────────────── def validate_key(api_key, model): if not api_key: return False, "API key required" if api_key not in API_KEYS: return False, "Invalid API key" kd = API_KEYS[api_key]; plan = kd.get("plan","free") cfg = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"]) exp = kd.get("expires_at") if exp and utcnow().timestamp() > exp: return False, "API key expired. Renew at HDRemover.com" if utcnow().timestamp() > kd.get("reset_at", 0): kd["calls_today"] = 0 kd["reset_at"] = (utcnow() + timedelta(days=1)).timestamp() now_ts = utcnow().timestamp() window = kd.setdefault("minute_window", deque()) while window and window[0] < now_ts - 60: window.popleft() if model not in cfg["models"]: return False, f"Model '{model}' not in {plan} plan. Upgrade at HDRemover.com" if kd.get("calls_today", 0) >= cfg["daily"]: return False, f"Daily limit {cfg['daily']} reached. Resets in 24h." if len(window) >= cfg.get("per_minute", 999): retry_in = int(60 - (now_ts - window[0])) + 1 return False, f"Rate limit: {cfg['per_minute']} req/min. Retry in {retry_in}s." return True, "" def _fire_webhook(url, payload): import requests as _req def _send(): try: _req.post(url, json=payload, timeout=5); logger.info(f"🔔 Webhook → {url}") except Exception as e: logger.warning(f"Webhook failed: {e}") threading.Thread(target=_send, daemon=True).start() def inc_usage(k): global _flush_counter if k not in API_KEYS: return with _keys_lock: kd = API_KEYS[k] kd["calls_today"] = kd.get("calls_today", 0) + 1 kd.setdefault("minute_window", deque()).append(utcnow().timestamp()) _flush_counter += 1 plan = kd.get("plan","free"); daily = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"])["daily"] webhook = kd.get("webhook_url","") if webhook and kd["calls_today"] == int(daily * 0.8): _fire_webhook(webhook, {"event":"usage_alert_80pct","owner":kd.get("owner",""), "plan":plan,"calls_today":kd["calls_today"],"daily_limit":daily, "key_hint":f"...{k[-6:]}","timestamp":utcnow().isoformat()}) _async_save(force=False) def get_usage(k): if k not in API_KEYS: return {"error":"Invalid key"} kd = API_KEYS[k]; plan = kd.get("plan","free"); cfg = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"]) return {"plan":plan,"calls_today":kd.get("calls_today",0), "daily_limit":cfg["daily"],"available_models":cfg["models"],"owner":kd.get("owner","")} def run_removal(api_key, img_data, model="fast", max_size=0): t0 = time.time() ok, err = validate_key(api_key, model) if not ok: return None, {}, err try: img = (Image.fromarray(img_data) if isinstance(img_data, np.ndarray) else img_data if isinstance(img_data, Image.Image) else Image.open(img_data)).convert("RGB") ow, oh = img.size effective_max = max_size if max_size > 0 else MAX_IMAGE_PX if max(ow, oh) > effective_max: r = effective_max / max(ow, oh) img = img.resize((int(ow*r), int(oh*r)), Image.LANCZOS) result = MODEL_FN[model](img) inc_usage(api_key) elapsed = round(time.time()-t0, 2) meta = {"model":MODEL_INFO[model]["name"],"model_key":model, "original_size":f"{ow}x{oh}","output_size":f"{result.size[0]}x{result.size[1]}", "processing_time_sec":elapsed,"calls_today":API_KEYS[api_key]["calls_today"]} logger.info(f"✅ {model} {ow}x{oh} {elapsed}s key=...{api_key[-6:]}") return result, meta, "" except Exception as e: logger.error(f"run_removal: {e}", exc_info=True) return None, {}, str(e) # ── Admin ────────────────────────────────────────────────────────────────────── import gradio as gr _CHOICES = ["🚀 Fast (U2-Net) ~1-5s","⚡ Quality (BiRefNet-lite) ~5-8s", "💇 Hair & Portrait (BEN2) ~10-15s","🏆 Best Quality (RMBG-2.0) ~20s"] _CMAP = {"🚀 Fast (U2-Net) ~1-5s":"fast","⚡ Quality (BiRefNet-lite) ~5-8s":"quality", "💇 Hair & Portrait (BEN2) ~10-15s":"hair","🏆 Best Quality (RMBG-2.0) ~20s":"best"} def ui_usage(api_key): info = get_usage(api_key) if "error" in info: return f"❌ {info['error']}" return (f"**Plan:** {info['plan'].upper()} \n**Calls today:** {info['calls_today']} / {info['daily_limit']} \n" f"**Models:** {', '.join(info['available_models'])} \n**Owner:** {info.get('owner','—')}") def admin_login(password): if password == ADMIN_PASS: return gr.update(visible=False), gr.update(visible=True), "", admin_list_customers(), admin_stats() return gr.update(visible=True), gr.update(visible=False), "❌ Wrong password", [], "" def admin_list_customers(): rows = [] for key, d in API_KEYS.items(): if key == MASTER_KEY: continue exp = d.get("expires_at") exp_str = datetime.fromtimestamp(exp, tz=timezone.utc).strftime("%Y-%m-%d") if exp else "Never" rows.append([key, d.get("owner",""), d.get("plan","free"), d.get("calls_today",0), d.get("limit", PLAN_LIMITS.get(d.get("plan","free"),{}).get("daily",10)), d.get("created_at","")[:10] if d.get("created_at") else "", exp_str, "✅" if d.get("webhook_url") else "—"]) return rows def admin_add_customer(email, plan, custom_limit, expiry_days="", webhook_url=""): if not email or "@" not in email: return "❌ Valid email required", admin_list_customers(), "" if plan not in PLAN_LIMITS: return "❌ Invalid plan", admin_list_customers(), "" key = gen_key(plan) limit = int(custom_limit) if str(custom_limit).strip().isdigit() else PLAN_LIMITS[plan]["daily"] days = int(expiry_days) if str(expiry_days).strip().isdigit() and int(expiry_days) > 0 else PLAN_DEFAULT_EXPIRY.get(plan) expires_at = (utcnow() + timedelta(days=days)).timestamp() if days else None API_KEYS[key] = {"plan":plan,"owner":email,"calls_today":0,"reset_at":0,"limit":limit, "models":PLAN_LIMITS[plan]["models"],"created_at":utcnow().isoformat(), "expires_at":expires_at,"webhook_url":webhook_url.strip() if webhook_url else ""} ok, msg = push_keys_to_hf_secret() return (f"✅ Key for **{email}** ({plan})\n\n🔑 `{key}`\n⏳ Expires: **{f'{days} days' if days else 'Never'}**\n\n{msg}", admin_list_customers(), key) def admin_delete_customer(key): if not key or key.strip() not in API_KEYS: return "❌ Key not found", admin_list_customers() key = key.strip() if key == MASTER_KEY: return "❌ Cannot delete master key", admin_list_customers() owner = API_KEYS[key].get("owner",""); del API_KEYS[key] ok, msg = push_keys_to_hf_secret() return f"✅ Deleted **{owner}**\n\n{msg}", admin_list_customers() def admin_upgrade_plan(key, new_plan): if not key or key.strip() not in API_KEYS: return "❌ Key not found", admin_list_customers() if new_plan not in PLAN_LIMITS: return "❌ Invalid plan", admin_list_customers() key = key.strip() API_KEYS[key]["plan"] = new_plan; API_KEYS[key]["limit"] = PLAN_LIMITS[new_plan]["daily"] API_KEYS[key]["models"] = PLAN_LIMITS[new_plan]["models"] ok, msg = push_keys_to_hf_secret() return f"✅ **{API_KEYS[key].get('owner','')}** → **{new_plan}**\n\n{msg}", admin_list_customers() def admin_stats(): vals = list(API_KEYS.values()) total = len([v for v in vals if v.get("owner") != "HDRemover.com"]) free_c = len([v for v in vals if v.get("plan")=="free"]) start = len([v for v in vals if v.get("plan")=="starter"]) pro_c = len([v for v in vals if v.get("plan") in ("pro","master") and v.get("owner")!="HDRemover.com"]) calls = sum(v.get("calls_today",0) for v in vals) return (f"👥 **Total:** {total} \n🆓 **Free:** {free_c} \n⭐ **Starter:** {start} \n" f"🏆 **Pro:** {pro_c} \n📊 **Calls Today:** {calls} \n💰 **Est. MRR:** ${start*9+pro_c*29}") def admin_export_json(): safe = {} for k, v in API_KEYS.items(): entry = dict(v) if "minute_window" in entry: entry["minute_window"] = list(entry["minute_window"]) safe[k] = entry return json.dumps(safe, indent=2, default=str) # ── BG Helper ────────────────────────────────────────────────────────────────── import zipfile, tempfile _session_history: dict = {} def _history_add(api_key, entry): if api_key not in _session_history: _session_history[api_key] = [] _session_history[api_key].append(entry) if len(_session_history[api_key]) > 50: _session_history[api_key].pop(0) def _history_get(api_key): return _session_history.get(api_key, []) def _history_clear(api_key): _session_history[api_key] = [] def _apply_background(result_rgba, bg_choice, bg_color, bg_image=None): if bg_choice == "transparent": return result_rgba w, h = result_rgba.size if bg_choice == "color": try: from PIL import ImageColor; rgb = ImageColor.getrgb(bg_color) except: rgb = (255,255,255) canvas = Image.new("RGBA",(w,h),rgb+(255,)); canvas.paste(result_rgba,mask=result_rgba.split()[3]) return canvas.convert("RGB") if bg_choice == "custom_image" and bg_image is not None: bg = (Image.fromarray(bg_image) if isinstance(bg_image,np.ndarray) else bg_image).convert("RGBA") bg = bg.resize((w,h),Image.LANCZOS); bg.paste(result_rgba,mask=result_rgba.split()[3]) return bg.convert("RGB") return result_rgba def ui_remove_with_bg(api_key, image, choice, bg_choice, bg_color, bg_image): m = _CMAP.get(choice,"fast") result, meta, err = run_removal(api_key, image, m) if err: return None, f"❌ **Error:** {err}" final = _apply_background(result, bg_choice, bg_color, bg_image) _history_add(api_key or "anon", {"name":"single_image","time":utcnow().strftime("%H:%M:%S"), "model":meta["model"],"size":meta["output_size"],"result":final}) return final, (f"✅ **Done** | `{meta['model']}` | `{meta['processing_time_sec']}s` | " f"`{meta['original_size']}` → `{meta['output_size']}` | calls: `{meta['calls_today']}`") def ui_batch_remove(api_key, images, choice, bg_choice, bg_color, bg_image, progress=gr.Progress()): if not images: return [], "⚠️ No images uploaded.", None model = _CMAP.get(choice,"fast"); results_gallery=[]; status_lines=[]; processed_pils=[] progress(0, desc="Starting batch…") for i, img_input in enumerate(images): fname = f"image_{i+1}" try: if isinstance(img_input, str): pil_in = Image.open(img_input).convert("RGB"); fname = os.path.basename(img_input) elif isinstance(img_input, np.ndarray): pil_in = Image.fromarray(img_input).convert("RGB") elif isinstance(img_input, Image.Image): pil_in = img_input.convert("RGB") else: status_lines.append(f"❌ `{fname}` — unsupported"); continue except Exception as e: status_lines.append(f"❌ `{fname}` — {e}"); progress((i+1)/len(images)); continue progress(i/len(images), desc=f"Processing {i+1}/{len(images)}: {fname}") result, meta, err = run_removal(api_key, pil_in, model) if err: status_lines.append(f"❌ `{fname}` — {err}"); progress((i+1)/len(images)); continue final = _apply_background(result, bg_choice, bg_color, bg_image) _history_add(api_key or "anon", {"name":fname,"time":utcnow().strftime("%H:%M:%S"), "model":meta["model"],"size":meta["output_size"],"result":final}) results_gallery.append(final); processed_pils.append((fname,final)) status_lines.append(f"✅ `{fname}` — {meta['processing_time_sec']}s | {meta['output_size']}") progress((i+1)/len(images), desc=f"Done: {fname}") zip_path = None if processed_pils: tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".zip") with zipfile.ZipFile(tmp.name,"w",zipfile.ZIP_DEFLATED) as zf: for fname, pil in processed_pils: buf = io.BytesIO(); ext = "png" if bg_choice=="transparent" else "jpg" pil.save(buf,"PNG" if bg_choice=="transparent" else "JPEG",optimize=True); buf.seek(0) zf.writestr(os.path.splitext(fname)[0]+f"_nobg.{ext}", buf.read()) zip_path = tmp.name total = len(processed_pils); failed = len(images)-total return results_gallery, f"### Batch Complete\n✅ **{total}** processed | ❌ **{failed}** failed\n\n"+"\n".join(status_lines), zip_path def ui_get_history(api_key): items = _history_get(api_key or "anon") if not items: return [], "No images processed yet." gallery = [item["result"] for item in items] lines = [f"| `{it['name']}` | {it['model']} | {it['size']} | {it['time']} |" for it in items] return gallery, f"### History ({len(items)})\n\n| File | Model | Size | Time |\n|---|---|---|---|\n"+"\n".join(lines) def ui_clear_history(api_key): _history_clear(api_key or "anon"); return [], "🗑️ Cleared." # ── Gradio UI ────────────────────────────────────────────────────────────────── with gr.Blocks(title="HDRemover — Background Removal API") as demo: gr.HTML("""
4 AI Models · API Key Auth · HDRemover.com