Spaces:
Running
Running
| """ | |
| 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("""<div style="text-align:center;padding:18px 0 8px"> | |
| <h1 style="font-size:26px;font-weight:900;margin:0">π¨ HDRemover β Background Removal API</h1> | |
| <p style="color:#888;font-size:13px;margin:6px 0 0">4 AI Models Β· API Key Auth Β· | |
| <a href="https://hdremover.com" target="_blank" style="color:#00e5a0">HDRemover.com</a></p> | |
| </div>""") | |
| with gr.Tabs(): | |
| with gr.TabItem("πΌοΈ Remove Background"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| t_key = gr.Textbox(label="π API Key", type="password", placeholder="hdremover-xxxx-xxxx") | |
| t_model = gr.Dropdown(label="Model", choices=_CHOICES, value=_CHOICES[0]) | |
| t_img = gr.Image(label="Upload Image", type="pil") | |
| t_btn = gr.Button("β¨ Remove Background", variant="primary", size="lg") | |
| with gr.Column(): | |
| t_out = gr.Image(label="Result β Transparent PNG", type="pil") | |
| t_info = gr.Markdown("*Upload an image and click Remove Background*") | |
| with gr.Accordion("π¨ Background Options (optional)", open=False): | |
| t_bg_choice = gr.Radio(label="Output Background", choices=["transparent","color","custom_image"], value="transparent") | |
| t_bg_color = gr.ColorPicker(label="Color", value="#ffffff", visible=False) | |
| t_bg_image = gr.Image(label="Custom Background", type="pil", visible=False) | |
| def _t_toggle(c): return gr.update(visible=c=="color"), gr.update(visible=c=="custom_image") | |
| t_bg_choice.change(_t_toggle, [t_bg_choice], [t_bg_color, t_bg_image]) | |
| t_btn.click(ui_remove_with_bg, [t_key,t_img,t_model,t_bg_choice,t_bg_color,t_bg_image], [t_out,t_info]) | |
| gr.Markdown("---\n### Plans\n| Plan | Daily | Models | Price |\n|---|---|---|---|\n| Free | 10/day | fast | $0 |\n| Starter | 100/day | fast+quality+hair | $9/mo |\n| Pro | 500/day | All 4 | $29/mo |\n\nπ§ **support@hdremover.com**") | |
| with gr.TabItem("π My Usage"): | |
| u_key = gr.Textbox(label="π Your API Key", type="password") | |
| u_btn = gr.Button("Check Usage", variant="secondary") | |
| u_out = gr.Markdown() | |
| u_btn.click(ui_usage, [u_key], [u_out]) | |
| with gr.TabItem("π API Docs"): | |
| gr.Markdown("""## REST API\n**Base URL:** `https://hdremover-background-remover.hf.space`\n\n### POST /api/remove-bg\n```\nHeader: X-API-Key: your-key\nBody: image=<file>, model=fast|quality|hair|best\n```\n\n### GET /api/usage\n```\nHeader: X-API-Key: your-key\n```\n\n### GET /health\nReturns server status and loaded models.\n\n### cURL Example\n```bash\ncurl -X POST https://hdremover-background-remover.hf.space/api/remove-bg \\\n -H "X-API-Key: your-key" \\\n -F "image=@photo.jpg" \\\n -F "model=fast" \\\n --output result.png\n```""") | |
| with gr.TabItem("π Admin"): | |
| with gr.Column(visible=True) as admin_login_col: | |
| gr.Markdown("### π Admin Login") | |
| a_pwd = gr.Textbox(label="Password", type="password") | |
| a_lbtn = gr.Button("Login", variant="primary") | |
| a_lerr = gr.Markdown() | |
| with gr.Column(visible=False) as admin_panel_col: | |
| gr.Markdown("## π Admin Panel") | |
| with gr.Tabs(): | |
| with gr.TabItem("β Add Customer"): | |
| with gr.Row(): | |
| a_email = gr.Textbox(label="Email", scale=2) | |
| a_plan = gr.Dropdown(label="Plan", choices=["free","starter","pro","master"], value="starter", scale=1) | |
| a_limit = gr.Textbox(label="Custom Limit (optional)", scale=1) | |
| with gr.Row(): | |
| a_expiry = gr.Textbox(label="Expires In (days)", scale=1) | |
| a_webhook = gr.Textbox(label="Webhook URL (optional)", scale=3) | |
| a_add_btn = gr.Button("β¨ Generate Key + Save", variant="primary") | |
| a_add_out = gr.Markdown() | |
| a_new_key = gr.Textbox(label="π Generated Key", interactive=True) | |
| with gr.TabItem("π₯ Customers"): | |
| a_refresh_btn = gr.Button("π Refresh", size="sm") | |
| a_table = gr.Dataframe( | |
| headers=["API Key","Owner","Plan","Calls","Limit","Created","Expires","Webhook"], | |
| datatype=["str","str","str","number","number","str","str","str"], interactive=False, wrap=True) | |
| with gr.Row(): | |
| a_edit_key = gr.Textbox(label="Key to Edit") | |
| a_edit_plan = gr.Dropdown(label="New Plan", choices=["free","starter","pro","master"], value="pro") | |
| a_upgrade_btn = gr.Button("β¬οΈ Upgrade Plan", variant="secondary") | |
| a_upgrade_out = gr.Markdown() | |
| with gr.Row(): | |
| a_del_key = gr.Textbox(label="Key to Delete") | |
| a_del_btn = gr.Button("ποΈ Delete", variant="stop") | |
| a_del_out = gr.Markdown() | |
| with gr.TabItem("π Stats"): | |
| a_stats_btn = gr.Button("π Refresh Stats") | |
| a_stats_out = gr.Markdown() | |
| a_stats_btn.click(admin_stats, [], [a_stats_out]) | |
| with gr.TabItem("π Export JSON"): | |
| a_export_btn = gr.Button("π Export") | |
| a_export_out = gr.Code(language="json", interactive=False) | |
| a_export_btn.click(admin_export_json, [], [a_export_out]) | |
| a_lbtn.click(admin_login, [a_pwd], [admin_login_col,admin_panel_col,a_lerr,a_table,a_stats_out]) | |
| a_pwd.submit(admin_login, [a_pwd], [admin_login_col,admin_panel_col,a_lerr,a_table,a_stats_out]) | |
| a_add_btn.click(admin_add_customer, [a_email,a_plan,a_limit,a_expiry,a_webhook], [a_add_out,a_table,a_new_key]) | |
| a_refresh_btn.click(lambda: admin_list_customers(), [], [a_table]) | |
| a_upgrade_btn.click(admin_upgrade_plan, [a_edit_key,a_edit_plan], [a_upgrade_out,a_table]) | |
| a_del_btn.click(admin_delete_customer, [a_del_key], [a_del_out,a_table]) | |
| with gr.TabItem("π¦ Batch Processing"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| b_key = gr.Textbox(label="π API Key", type="password") | |
| b_model = gr.Dropdown(label="Model", choices=_CHOICES, value=_CHOICES[0]) | |
| b_files = gr.File(label="Upload Images", file_count="multiple", file_types=["image"]) | |
| b_bg_choice = gr.Radio(label="Output BG", choices=["transparent","color","custom_image"], value="transparent") | |
| b_bg_color = gr.ColorPicker(label="Color", value="#ffffff", visible=False) | |
| b_bg_image = gr.Image(label="Custom BG", type="pil", visible=False) | |
| b_btn = gr.Button("π Start Batch", variant="primary", size="lg") | |
| with gr.Column(): | |
| b_gallery = gr.Gallery(label="Results", columns=3, object_fit="contain", height=360) | |
| b_status = gr.Markdown("*Upload images and click Start Batch*") | |
| b_zip = gr.File(label="β¬οΈ Download ZIP") | |
| def _tb(c): return gr.update(visible=c=="color"), gr.update(visible=c=="custom_image") | |
| b_bg_choice.change(_tb, [b_bg_choice], [b_bg_color,b_bg_image]) | |
| b_btn.click(ui_batch_remove, [b_key,b_files,b_model,b_bg_choice,b_bg_color,b_bg_image], [b_gallery,b_status,b_zip]) | |
| with gr.TabItem("π¨ BG Replace"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| br_key = gr.Textbox(label="π API Key", type="password") | |
| br_model = gr.Dropdown(label="Model", choices=_CHOICES, value=_CHOICES[0]) | |
| br_img = gr.Image(label="Upload Image", type="pil") | |
| br_bg_choice = gr.Radio(label="New Background", choices=["transparent","color","custom_image"], value="color") | |
| br_bg_color = gr.ColorPicker(label="Color", value="#ffffff") | |
| br_bg_image = gr.Image(label="Custom BG Image", type="pil", visible=False) | |
| br_btn = gr.Button("β¨ Remove & Replace", variant="primary", size="lg") | |
| with gr.Column(): | |
| br_out = gr.Image(label="Result", type="pil") | |
| br_info = gr.Markdown("*Upload an image and choose a background*") | |
| def _brb(c): return gr.update(visible=c=="color"), gr.update(visible=c=="custom_image") | |
| br_bg_choice.change(_brb, [br_bg_choice], [br_bg_color,br_bg_image]) | |
| br_btn.click(ui_remove_with_bg, [br_key,br_img,br_model,br_bg_choice,br_bg_color,br_bg_image], [br_out,br_info]) | |
| with gr.TabItem("π History"): | |
| with gr.Row(): | |
| h_refresh_btn = gr.Button("π Refresh", variant="secondary") | |
| h_clear_btn = gr.Button("ποΈ Clear", variant="stop") | |
| h_gallery = gr.Gallery(label="Processed Images", columns=4, object_fit="contain", height=400) | |
| h_status = gr.Markdown("*Click Refresh to load history*") | |
| h_key = gr.Textbox(label="π API Key", type="password") | |
| h_refresh_btn.click(ui_get_history, [h_key], [h_gallery,h_status]) | |
| h_clear_btn.click(ui_clear_history, [h_key], [h_gallery,h_status]) | |
| # ββ FastAPI Routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from fastapi import Request | |
| from fastapi.responses import Response, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| demo.app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["GET","POST","OPTIONS"], | |
| allow_headers=["*"], | |
| expose_headers=["X-Model","X-Processing-Time","X-Original-Size","X-Output-Size","X-Calls-Today"], | |
| ) | |
| async def api_remove_bg(request: Request): | |
| client_ip = request.headers.get("x-forwarded-for", request.client.host or "unknown").split(",")[0].strip() | |
| if _check_brute_force(client_ip): | |
| return JSONResponse({"success":False,"error":"Too many failed attempts."}, status_code=429) | |
| x_api_key = request.headers.get("x-api-key") or request.headers.get("X-API-Key") | |
| if not x_api_key: | |
| _record_fail(client_ip) | |
| return JSONResponse({"success":False,"error":"X-API-Key header required"}, status_code=401) | |
| try: | |
| form = await request.form() | |
| model = str(form.get("model","fast")); max_size = int(form.get("max_size",0)) | |
| img_file = form.get("image") | |
| if not img_file: return JSONResponse({"success":False,"error":"'image' field required"}, status_code=400) | |
| raw = await img_file.read() | |
| if len(raw) > MAX_IMAGE_BYTES: | |
| return JSONResponse({"success":False,"error":f"File too large. Max {MAX_IMAGE_BYTES//(1024*1024)}MB."}, status_code=413) | |
| try: pil = Image.open(io.BytesIO(raw)).convert("RGB") | |
| except: return JSONResponse({"success":False,"error":"Invalid image file."}, status_code=400) | |
| except Exception as e: | |
| return JSONResponse({"success":False,"error":f"Bad request: {e}"}, status_code=400) | |
| if model not in MODEL_FN: | |
| return JSONResponse({"success":False,"error":f"Invalid model. Choose: {list(MODEL_FN)}"}, status_code=400) | |
| result, meta, err = run_removal(x_api_key, pil, model, max_size) | |
| if err: | |
| is_auth = any(w in err.lower() for w in ("key","limit","plan","upgrade","invalid","expired")) | |
| if is_auth: _record_fail(client_ip) | |
| return JSONResponse({"success":False,"error":err}, status_code=401 if is_auth else 500) | |
| buf = io.BytesIO(); result.save(buf,"PNG",optimize=True); buf.seek(0) | |
| return Response(buf.read(), media_type="image/png", headers={ | |
| "X-Model":meta["model"],"X-Processing-Time":str(meta["processing_time_sec"]), | |
| "X-Original-Size":meta["original_size"],"X-Output-Size":meta["output_size"], | |
| "X-Calls-Today":str(meta["calls_today"]), | |
| }) | |
| async def api_usage(request: Request): | |
| key = request.headers.get("x-api-key") or request.headers.get("X-API-Key") | |
| if not key: return JSONResponse({"error":"X-API-Key required"}, status_code=401) | |
| info = get_usage(key) | |
| return JSONResponse({"success":"error" not in info, **info}, status_code=200 if "error" not in info else 401) | |
| async def api_models(): | |
| return {"success":True,"models":{k:v for k,v in MODEL_INFO.items()}} | |
| async def health(): | |
| return {"status":"ok","hf_token":"set" if HF_TOKEN else "missing", | |
| "models_ready":[k for k in _cache if not k.endswith("_tf")], | |
| "timestamp":utcnow().isoformat()} | |
| # ββ Launch βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _periodic_flush(): | |
| import time as _t | |
| while True: | |
| _t.sleep(FLUSH_EVERY_S) | |
| save_keys_to_dataset(force=True) | |
| threading.Thread(target=_periodic_flush, daemon=True).start() | |
| logger.info("β° Periodic flush started") | |
| logger.info("Preloading U2-Net (blocking)...") | |
| load_u2net() | |
| def _bg_preload(): | |
| logger.info("Background preload: BiRefNet-lite...") | |
| load_birefnet() | |
| logger.info("Background preload: BEN2...") | |
| load_ben2() | |
| logger.info("Background preload: RMBG-2.0...") | |
| load_rmbg() | |
| logger.info("β All 4 models ready β zero cold starts") | |
| threading.Thread(target=_bg_preload, daemon=True).start() | |
| demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True, ssr_mode=False, strict_cors=False) |