import gradio as gr import numpy as np from PIL import Image import io import json import os import time import secrets import datetime from fastapi import Request from fastapi.responses import JSONResponse, Response # ── ZeroGPU / SD1.5 auto-detect ────────────────────────────────────────────── HAS_ZERO_GPU = False HAS_CUDA = False try: import spaces # Only enable if actually running on ZeroGPU hardware import os if os.environ.get("SPACE_HARDWARE") in ("gpu-t4-s", "gpu-t4-m", "gpu-a10g-s", "gpu-a10g-l", "zero-gpu"): HAS_ZERO_GPU = True print("✅ ZeroGPU detected — SD1.5 'best' mode will be available") else: print("ℹ️ spaces imported but no ZeroGPU hardware — LaMa-only mode") except ImportError: print("ℹ️ No ZeroGPU — LaMa-only mode (fast + quality)") try: import torch HAS_CUDA = torch.cuda.is_available() except: pass # ── HF Secret auto-update (like FreeBG) ────────────────────────────────────── HF_TOKEN = os.environ.get("HF_TOKEN", "") HF_REPO_ID = os.environ.get("HF_REPO_ID", "") # e.g. "freebg/magic-eraser" def save_keys_to_hf(keys: dict) -> bool: """Push updated API_KEYS_JSON straight to HF Space Secret — Space restarts automatically.""" if not HF_TOKEN or not HF_REPO_ID: return False try: from huggingface_hub import HfApi api = HfApi(token=HF_TOKEN) api.add_space_secret( repo_id=HF_REPO_ID, key="API_KEYS_JSON", value=json.dumps(keys), ) return True except Exception as e: print(f"⚠️ HF Secret update failed: {e}") return False # ── LaMa model ──────────────────────────────────────────────────────────────── lama_model = None def load_lama(): global lama_model if lama_model is not None: return lama_model try: from simple_lama_inpainting import SimpleLama lama_model = SimpleLama() print("✅ LaMa loaded") return lama_model except Exception as e: print(f"❌ LaMa load error: {e}") return None # ── SD1.5 pipeline (ZeroGPU / CUDA only) ───────────────────────────────────── sd_pipe = None def load_sd(): return None # ── Key / Plan config ───────────────────────────────────────────────────────── MASTER_KEY = os.environ.get("MASTER_API_KEY", "freebg-master-change-this") ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin123") PLAN_LIMITS = { "free": {"daily": 10, "models": ["fast"]}, "starter": {"daily": 100, "models": ["fast", "quality"]}, "pro": {"daily": 500, "models": ["fast", "quality", "best"]}, "master": {"daily": 99999, "models": ["fast", "quality", "best"]}, } # In-memory keys cache (reloaded from env each call so secrets take effect after restart) usage_cache: dict = {} def get_today() -> str: return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d") def load_keys() -> dict: raw = os.environ.get("API_KEYS_JSON", "{}") try: return json.loads(raw) except: return {} def check_and_count(api_key: str, model: str) -> tuple[bool, str]: if api_key == MASTER_KEY: return True, "" keys = load_keys() if api_key not in keys: return False, "Invalid API key. Get one at freebg.site" info = keys[api_key] plan = info.get("plan", "free") lims = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"]) if model not in lims["models"]: return False, f"Model '{model}' not available on {plan} plan. Upgrade at freebg.site" today = get_today() cache_key = f"{api_key}:{today}" used = usage_cache.get(cache_key, 0) if used >= lims["daily"]: return False, f"Daily limit reached ({lims['daily']}/day on {plan} plan)" usage_cache[cache_key] = used + 1 return True, "" def get_usage_info(api_key: str) -> dict: """Always returns a dict with 'models' key — no KeyError possible.""" plan_info = PLAN_LIMITS.get("master") if api_key == MASTER_KEY: today = get_today() used = usage_cache.get(f"{api_key}:{today}", 0) return { "plan": "master", "daily_limit": plan_info["daily"], "used_today": used, "remaining": plan_info["daily"] - used, "models": plan_info["models"], "owner": "freebg.site", } keys = load_keys() if api_key not in keys: return {"error": "Invalid API key"} info = keys[api_key] plan = info.get("plan", "free") lims = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"]) today = get_today() used = usage_cache.get(f"{api_key}:{today}", 0) return { "plan": plan, "daily_limit": lims["daily"], "used_today": used, "remaining": max(0, lims["daily"] - used), "models": lims["models"], "owner": info.get("owner", ""), } # ── Inpainting ──────────────────────────────────────────────────────────────── def _prep_mask(mask: Image.Image) -> Image.Image: arr = np.array(mask.convert("L")) return Image.fromarray(((arr > 127).astype(np.uint8) * 255), "L") def run_lama(image: Image.Image, mask: Image.Image) -> Image.Image: lama = load_lama() if lama is None: raise RuntimeError("LaMa model failed to load") return lama(image.convert("RGB"), _prep_mask(mask)) def run_lama_quality(image: Image.Image, mask: Image.Image) -> Image.Image: from PIL import ImageFilter return run_lama(image, mask).filter(ImageFilter.UnsharpMask(radius=1.5, percent=120, threshold=3)) def run_best(image: Image.Image, mask: Image.Image) -> Image.Image: return run_lama_quality(image, mask) if HAS_ZERO_GPU: import spaces as _spaces run_lama = _spaces.GPU(run_lama) run_lama_quality = _spaces.GPU(run_lama_quality) run_best = _spaces.GPU(run_best) # ── Gradio helpers ──────────────────────────────────────────────────────────── def erase_ui(composite, model: str, api_key: str): if composite is None: return None, "❌ Please upload and paint an image first" background = composite.get("background") layers = composite.get("layers", []) if background is None: return None, "❌ No image provided" image = background.convert("RGB") if not layers: return None, "❌ Please paint over the area to erase" r, g, b, a = layers[0].convert("RGBA").split() mask = a if np.array(mask).max() == 0: return None, "❌ No brush strokes found — paint over the area to erase" key = (api_key or "").strip() or MASTER_KEY allowed, err = check_and_count(key, model.lower()) if not allowed: return None, f"❌ {err}" try: t0 = time.time() fn = {"fast": run_lama, "quality": run_lama_quality, "best": run_best}.get(model, run_lama) result = fn(image, mask) return result, f"✅ Done in {time.time()-t0:.1f}s | Model: {model}" except Exception as e: return None, f"❌ Error: {e}" def check_usage_ui(api_key: str) -> str: key = (api_key or "").strip() if not key: return "❌ Enter your API key above" info = get_usage_info(key) if "error" in info: return f"❌ {info['error']}" return ( f"**Plan:** {info['plan'].upper()}\n\n" f"**Calls today:** {info['used_today']} / {info['daily_limit']}\n\n" f"**Remaining:** {info['remaining']}\n\n" f"**Models:** {', '.join(info['models'])}\n\n" f"**Owner:** {info['owner']}" ) # ── Admin helpers ───────────────────────────────────────────────────────────── def _auth(pw: str) -> bool: return pw == ADMIN_PASSWORD def admin_add_customer(pw, email, plan, custom_limit): if not _auth(pw): return "❌ Wrong password", "", "" if plan not in PLAN_LIMITS: return f"❌ Invalid plan", "", "" prefix = {"free":"me-fr","starter":"me-st","pro":"me-pr","master":"me-ma"}[plan] key = f"freebg-{prefix}-{secrets.token_hex(4)}-{secrets.token_hex(4)}" keys = load_keys() entry = {"owner": email, "plan": plan, "created": get_today(), "active": True} if custom_limit: try: entry["custom_daily"] = int(custom_limit) except: pass keys[key] = entry saved = save_keys_to_hf(keys) hf_note = "✅ Auto-saved to HF Secret. Space restarts in ~30s." if saved else "⚠️ HF auto-save failed — paste JSON manually." return f"✅ Key created for {email} ({plan})\n\nKey: **{key}**\n\n{hf_note}", key, json.dumps(keys, indent=2) def admin_get_customers(pw): if not _auth(pw): return "❌ Wrong password" keys = load_keys() today = get_today() rows = [] for k, v in keys.items(): used = usage_cache.get(f"{k}:{today}", 0) plan = v.get("plan","free") limit = v.get("custom_daily", PLAN_LIMITS.get(plan,{}).get("daily","?")) rows.append(f"| `{k}` | {v.get('owner','')} | {plan} | {used} | {limit} | {v.get('created','')} |") if not rows: return "No customers yet." header = "| API Key | Owner | Plan | Calls Today | Daily Limit | Created |\n|---|---|---|---|---|---|" return header + "\n" + "\n".join(rows) def admin_upgrade(pw, key, new_plan): if not _auth(pw): return "❌ Wrong password", "" keys = load_keys() if key not in keys: return "❌ Key not found", "" old = keys[key].get("plan","?") keys[key]["plan"] = new_plan saved = save_keys_to_hf(keys) note = "✅ Auto-saved to HF Secret." if saved else "⚠️ Paste JSON manually." return f"✅ {key}\n{old} → {new_plan}\n{note}", json.dumps(keys, indent=2) def admin_delete(pw, key): if not _auth(pw): return "❌ Wrong password", "" keys = load_keys() if key not in keys: return "❌ Key not found", "" del keys[key] saved = save_keys_to_hf(keys) note = "✅ Auto-saved to HF Secret." if saved else "⚠️ Paste JSON manually." return f"✅ Deleted: {key}\n{note}", json.dumps(keys, indent=2) def admin_stats(pw): if not _auth(pw): return "❌ Wrong password" keys = load_keys() today = get_today() plans = {"free":0,"starter":0,"pro":0} calls = 0 for k, v in keys.items(): p = v.get("plan","free") if p in plans: plans[p] += 1 calls += usage_cache.get(f"{k}:{today}", 0) rev = plans["starter"]*9 + plans["pro"]*29 return ( f"👥 **Total Customers:** {len(keys)}\n\n" f"🆓 Free: {plans['free']}\n\n" f"⭐ Starter ($9): {plans['starter']}\n\n" f"🏆 Pro ($29): {plans['pro']}\n\n" f"📊 **Total Calls Today:** {calls}\n\n" f"💰 **Est. Monthly Revenue:** ${rev}" ) def admin_export_json(pw): if not _auth(pw): return "❌ Wrong password" return json.dumps(load_keys(), indent=2) # ── Build Gradio UI ─────────────────────────────────────────────────────────── gpu_note = "⚡ ZeroGPU active — best mode uses SD1.5 inpainting" if HAS_ZERO_GPU else "🖥️ CPU mode — fast & quality use LaMa. Best = LaMa fallback. Enable ZeroGPU for SD1.5." with gr.Blocks(title="🪄 FreeME — Magic Eraser API") as demo: gr.HTML("""
3 Models · API Key Auth · freebg.site
| Plan | Daily | Models | Price |
|---|---|---|---|
| Free | 10 | fast | $0 |
| Starter | 100 | fast + quality | $9/mo |
| Pro | 500 | all 3 | $29/mo |