import sys, os, time sys.path.insert(0, os.path.join(os.path.dirname(__file__), "smishing_detector")) import gradio as gr import spaces from predictor import SmishingPredictor from explainability.shap_explainer import SmishingExplainer CHECKPOINT = os.path.join(os.path.dirname(__file__), "smishing_detector", "best_model.pt") MODEL_REPO = os.getenv("SCAMSHIELD_MODEL_REPO", "ExistedYear/ScamShield-model") # SCAMSHIELD_GPU=0 serves entirely on CPU. Every @spaces.GPU call has to move # 1.11 GB of weights onto the metered GPU, so on the free tier that burns the # daily quota after a handful of scans. CPU inference is slower per message but # unmetered, which is the better trade for a demo that must not die. USE_GPU = os.getenv("SCAMSHIELD_GPU", "0").strip().lower() in {"1", "true", "yes", "on"} def gpu(fn): """Apply @spaces.GPU only when GPU serving is enabled.""" return spaces.GPU(fn) if USE_GPU else fn # ZeroGPU hardware refuses to start unless it finds at least one @spaces.GPU # function during startup ("No @spaces.GPU function detected"). This one is never # wired to an endpoint — it exists only so the runtime check passes while every # request below stays on unmetered CPU. @spaces.GPU def _zerogpu_startup_probe(): return "ok" print(f"Serving mode: {'GPU (ZeroGPU quota applies)' if USE_GPU else 'CPU (unmetered)'}") if not os.path.exists(CHECKPOINT): print(f"Checkpoint not found locally, downloading from {MODEL_REPO}...") from huggingface_hub import hf_hub_download import shutil os.makedirs(os.path.dirname(CHECKPOINT), exist_ok=True) shutil.copyfile(hf_hub_download(repo_id=MODEL_REPO, filename="best_model.pt"), CHECKPOINT) print(f"Weights downloaded to {CHECKPOINT}") print("Loading ScamShield model...") predictor = SmishingPredictor(CHECKPOINT) print("Loading SHAP explainer...") explainer = SmishingExplainer(predictor) print("Model loaded. Ready.") # Presence check only — never prints the key itself. from utils.safe_browsing import get_checker as _gsb print(f"GSB key configured: {bool(os.getenv('GOOGLE_SAFE_BROWSING_API_KEY'))} " f"| fallback domains: {len(_gsb().fallback_legit_domains)}") # ───────────────────────────────────────────────────────────────────────────── # Design system # ───────────────────────────────────────────────────────────────────────────── CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap'); .gradio-container{ --bg:#08080c; --bg2:#0e0e15; --panel:rgba(255,255,255,.032); --panel2:rgba(255,255,255,.055); --line:rgba(255,255,255,.085); --line2:rgba(255,255,255,.14); --ink:#fafafa; --muted:#a1a1aa; --faint:#6b6b76; --brand:#818cf8; --brand2:#c084fc; --hi:#fb7185; --hi-s:rgba(251,113,133,.13); --hi-b:rgba(251,113,133,.34); --md:#fbbf24; --md-s:rgba(251,191,36,.13); --md-b:rgba(251,191,36,.34); --lo:#34d399; --lo-s:rgba(52,211,153,.13); --lo-b:rgba(52,211,153,.34); background:var(--bg)!important; color:var(--ink)!important; font-family:'Inter',system-ui,sans-serif!important; max-width:1080px!important; margin:0 auto!important; padding:0!important; } .gradio-container *{font-family:'Inter',system-ui,sans-serif!important} .gradio-container .mono, .mono{font-family:'JetBrains Mono',monospace!important} body{background:var(--bg)!important} .gradio-container > footer, footer{display:none!important} .dark{--bg:#08080c} /* ── ambient glow ── */ .glow{position:fixed;inset:0;pointer-events:none;z-index:0; background: radial-gradient(620px 340px at 18% -8%, rgba(129,140,248,.16), transparent 62%), radial-gradient(520px 300px at 88% 4%, rgba(192,132,252,.12), transparent 60%); } .gradio-container{position:relative;z-index:1} /* ── nav ── */ .nav{display:flex;align-items:center;justify-content:space-between; padding:1.15rem 0 1.35rem;border-bottom:1px solid var(--line)} .logo{display:flex;align-items:center;gap:.6rem;font-weight:800;font-size:1.02rem;letter-spacing:-.02em} .logo-m{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;font-size:.95rem; background:linear-gradient(140deg,var(--brand),var(--brand2));color:#0b0b12; box-shadow:0 6px 20px rgba(129,140,248,.42)} .logo span{color:var(--brand)} .nav-r{display:flex;gap:.4rem;align-items:center;flex-wrap:wrap} .pill{display:inline-flex;align-items:center;gap:.35rem;font-size:.68rem;font-weight:600; padding:.3rem .68rem;border-radius:99px;border:1px solid var(--line2);color:var(--muted); background:var(--panel)} .pill-b{border-color:rgba(129,140,248,.4);color:var(--brand);background:rgba(129,140,248,.1)} .dotlive{width:6px;height:6px;border-radius:50%;background:var(--lo); box-shadow:0 0 0 3px rgba(52,211,153,.16)} /* ── hero ── */ .hero{padding:2.6rem 0 2.1rem;text-align:center} .eyebrow{display:inline-flex;align-items:center;gap:.45rem;font-size:.7rem;font-weight:700; letter-spacing:.06em;text-transform:uppercase;color:var(--brand); background:rgba(129,140,248,.1);border:1px solid rgba(129,140,248,.3); padding:.34rem .8rem;border-radius:99px;margin-bottom:1.15rem} .hero h1{font-size:clamp(2rem,5.2vw,3.35rem);font-weight:900;line-height:1.06; letter-spacing:-.045em;margin:0} .hero h1 em{font-style:normal;background:linear-gradient(100deg,var(--brand),var(--brand2)); -webkit-background-clip:text;background-clip:text;color:transparent} .hero p{color:var(--muted);font-size:1rem;line-height:1.6;margin:.85rem auto 0;max-width:640px} /* ── surfaces ── */ .card{background:var(--panel);border:1px solid var(--line);border-radius:18px; padding:1.35rem 1.45rem;backdrop-filter:blur(8px)} .card-t{font-size:.7rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase; color:var(--faint);margin-bottom:.9rem;display:flex;align-items:center;gap:.45rem} .card-t::before{content:'';width:3px;height:12px;border-radius:2px; background:linear-gradient(var(--brand),var(--brand2))} /* ── input ── */ .ta textarea{background:var(--bg2)!important;border:1.5px solid var(--line2)!important; border-radius:14px!important;color:var(--ink)!important;font-size:.92rem!important; line-height:1.6!important;padding:.9rem 1rem!important;resize:vertical} .ta textarea:focus{border-color:var(--brand)!important;box-shadow:0 0 0 4px rgba(129,140,248,.14)!important} .ta textarea::placeholder{color:var(--faint)!important} .scan-btn button{background:linear-gradient(135deg,var(--brand),var(--brand2))!important; border:none!important;color:#0b0b12!important;font-weight:800!important;font-size:.92rem!important; border-radius:13px!important;height:44px;box-shadow:0 8px 26px rgba(129,140,248,.34)!important; transition:transform .15s, box-shadow .15s} .scan-btn button:hover{transform:translateY(-1px);box-shadow:0 12px 32px rgba(129,140,248,.44)!important} /* examples strip */ .examples{border:0!important;background:transparent!important;padding:0!important;gap:.35rem!important} .examples table{display:none!important} .examples .label,.examples span,.examples>div>span{display:none!important} .examples button{background:var(--panel)!important;border:1px solid var(--line)!important; color:var(--muted)!important;font-size:.74rem!important;font-weight:500!important; border-radius:99px!important;padding:.4rem .85rem!important;text-align:left!important; max-width:100%;line-height:1.4;white-space:normal!important;height:auto!important} .examples button:hover{border-color:var(--brand)!important;color:var(--ink)!important; background:rgba(129,140,248,.1)!important} /* ── verdict ── */ .v{border-radius:18px;padding:1.3rem 1.4rem;border:1px solid var(--line2); background:var(--panel);position:relative;overflow:hidden} .v::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px} .v-hi{border-color:var(--hi-b);background:linear-gradient(180deg,var(--hi-s),transparent 60%)} .v-hi::before{background:var(--hi)} .v-md{border-color:var(--md-b);background:linear-gradient(180deg,var(--md-s),transparent 60%)} .v-md::before{background:var(--md)} .v-lo{border-color:var(--lo-b);background:linear-gradient(180deg,var(--lo-s),transparent 60%)} .v-lo::before{background:var(--lo)} .v-top{display:flex;align-items:flex-start;gap:.9rem} .v-ic{width:46px;height:46px;border-radius:13px;display:grid;place-items:center;font-size:1.3rem; flex-shrink:0;border:1px solid} .ic-hi{background:var(--hi-s);border-color:var(--hi-b)} .ic-md{background:var(--md-s);border-color:var(--md-b)} .ic-lo{background:var(--lo-s);border-color:var(--lo-b)} .v-title{font-size:1.28rem;font-weight:800;letter-spacing:-.025em;line-height:1.25} .v-sub{font-size:.8rem;color:var(--muted);margin-top:.22rem;display:flex;gap:.5rem; align-items:center;flex-wrap:wrap} .v-score{margin-left:auto;text-align:right;line-height:1;flex-shrink:0} .v-score b{font-size:2.3rem;font-weight:900;letter-spacing:-.05em} .v-score i{font-style:normal;font-size:.9rem;font-weight:700;color:var(--faint);margin-left:1px} .hi .v-score b{color:var(--hi)} .md .v-score b{color:var(--md)} .lo .v-score b{color:var(--lo)} .meter{position:relative;height:8px;border-radius:99px;background:rgba(255,255,255,.07); margin-top:1.15rem;overflow:visible} .meter-f{height:100%;border-radius:99px;transition:width 1s cubic-bezier(.22,1,.36,1)} .meter-t{position:absolute;top:-4px;bottom:-4px;width:2px;background:var(--ink);opacity:.55; border-radius:2px} .meter-t::after{content:'55';position:absolute;top:-15px;left:50%;transform:translateX(-50%); font-size:.6rem;font-weight:700;color:var(--faint)} .meter-s{display:flex;justify-content:space-between;margin-top:.5rem;font-size:.66rem;color:var(--faint)} /* ── signal chips ── */ .chips{display:flex;flex-wrap:wrap;gap:.35rem} .chip{font-size:.71rem;font-weight:600;padding:.32rem .7rem;border-radius:9px; border:1px solid var(--line2);background:var(--panel2);color:var(--muted)} .ch-hi{border-color:var(--hi-b);background:var(--hi-s);color:var(--hi)} .ch-md{border-color:var(--md-b);background:var(--md-s);color:var(--md)} .ch-lo{border-color:var(--lo-b);background:var(--lo-s);color:var(--lo)} .ch-off{opacity:.42} .rows{display:flex;flex-direction:column;gap:.42rem} .row{display:flex;gap:.6rem;align-items:flex-start;font-size:.8rem;color:var(--muted);line-height:1.6} .row b{color:var(--ink);font-weight:600} /* ── SHAP ── */ .sh{display:flex;flex-direction:column;gap:.42rem} .sh-spin{width:13px;height:13px;border:2px solid var(--line2);border-top-color:var(--brand); border-radius:50%;animation:shspin .7s linear infinite;display:inline-block;margin-right:.5rem; vertical-align:-2px} @keyframes shspin{to{transform:rotate(360deg)}} .sh-row{display:flex;align-items:center;gap:.7rem} .sh-w{width:120px;text-align:right;font-size:.74rem;font-weight:600;color:var(--muted); white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .sh-t{flex:1;position:relative;height:20px} .sh-t::before{content:'';position:absolute;left:50%;top:-3px;bottom:-3px;width:1px; background:var(--line2)} .sh-b{position:absolute;height:11px;border-radius:4px;top:4px} .sh-v{width:52px;font-size:.7rem;color:var(--faint);font-variant-numeric:tabular-nums} .sh-lg{display:flex;gap:1rem;font-size:.66rem;color:var(--faint);margin-top:.7rem; padding-top:.6rem;border-top:1px solid var(--line)} .sh-lg i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:.3rem} /* ── alert ── */ .alert{border-radius:14px;padding:.95rem 1.1rem;font-size:.81rem;line-height:1.75; color:var(--muted);border:1px solid var(--hi-b);background:var(--hi-s)} .alert b{color:var(--ink)} .alert code{background:rgba(0,0,0,.32);border:1px solid var(--line2);border-radius:5px; padding:.05rem .35rem;color:var(--ink);font-size:.78rem} .alert-g{border-color:var(--lo-b);background:var(--lo-s)} /* ── stats strip ── */ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:.6rem} .stat{background:var(--panel2);border:1px solid var(--line);border-radius:13px;padding:.8rem .9rem} .stat b{display:block;font-size:1.5rem;font-weight:800;letter-spacing:-.03em;line-height:1.1} .stat span{font-size:.65rem;color:var(--faint);text-transform:uppercase;letter-spacing:.07em; font-weight:600} /* ── phone ── */ .mob{display:grid;grid-template-columns:396px 1fr;gap:1.6rem;align-items:start;margin-top:.4rem} @media(max-width:900px){.mob{grid-template-columns:1fr}} .phone{background:#0b0b12;border:9px solid #05050a;border-radius:38px;padding:1rem .85rem 1.2rem; box-shadow:0 26px 70px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.06)} .notch{width:112px;height:17px;background:#05050a;border-radius:0 0 11px 11px;margin:0 auto .8rem} .ph-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem} .ph-brand{font-size:.86rem;font-weight:800;display:flex;align-items:center;gap:.4rem} .ph-brand span{color:var(--brand)} .ph-tabs{display:flex;gap:.2rem;background:rgba(255,255,255,.05);border-radius:9px;padding:.2rem; border:1px solid var(--line)} .pt{font-size:.68rem;font-weight:700;color:var(--faint);background:transparent;border:none; border-radius:7px;padding:.26rem .58rem} .pt-a{background:rgba(255,255,255,.09);color:var(--ink)} .in-list{border:1px solid var(--line);border-radius:13px;overflow:hidden;max-height:330px; overflow-y:auto} .in-r{display:flex;gap:.6rem;align-items:flex-start;padding:.65rem .7rem; border-bottom:1px solid var(--line);cursor:pointer;transition:background .15s} .in-r:last-child{border-bottom:none} .in-r:hover{background:rgba(255,255,255,.04)} .in-d{width:7px;height:7px;border-radius:50%;margin-top:.42rem;flex-shrink:0;background:var(--faint)} .in-b{flex:1;min-width:0} .in-s{font-size:.68rem;font-weight:700;color:var(--ink);display:flex;gap:.4rem;align-items:center} .in-s em{font-style:normal;font-size:.6rem;font-weight:700;color:var(--faint); text-transform:uppercase;letter-spacing:.05em} .in-x{font-size:.73rem;color:var(--muted);line-height:1.45;margin-top:.15rem; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} .in-l{font-size:.57rem;font-weight:800;letter-spacing:.05em;padding:.14rem .42rem;border-radius:99px; flex-shrink:0;margin-top:.15rem} .ph-note{font-size:.72rem;color:var(--faint);line-height:1.65;margin:.7rem 0 0;text-align:center} /* ── footer ── */ .foot{text-align:center;padding:2.4rem 0 1.6rem;border-top:1px solid var(--line);margin-top:2.2rem} .foot p{font-size:.73rem;color:var(--faint);line-height:1.9} .foot b{color:var(--muted);font-weight:600} /* gradio tab styling */ .tabs > .tab-nav > button{border-radius:10px!important;font-weight:700!important;font-size:.85rem!important; color:var(--faint)!important;padding:.55rem 1rem!important} .tabs > .tab-nav > button.selected{color:var(--ink)!important;background:var(--panel2)!important} .block-label,.label-wrap span{font-size:.7rem!important;font-weight:700!important; letter-spacing:.05em;text-transform:uppercase;color:var(--faint)!important} /* hidden plumbing for the JSON API */ .api-hidden{display:none!important} """ # ───────────────────────────────────────────────────────────────────────────── # Seeded inbox (used by the Mobile tab; browsers/iOS cannot read a real inbox) # ───────────────────────────────────────────────────────────────────────────── SEED_SMS = [ {"s": "SBI-ALERT", "n": "+91-SBI", "b": "URGENT: Your SBI account has been suspended. Verify KYC now at http://sbi-kyc-verify.xyz or funds will be frozen.", "e": "spam"}, {"s": "Lottery", "n": "+91-VD", "b": "CONGRATULATIONS! You have been selected as the lucky winner of Rs.5,00,000. Claim before it expires: http://prize-winner.xyz/claim", "e": "spam"}, {"s": "DHL", "n": "+91-DHL", "b": "Your package could not be delivered. Pay Rs.2.99 customs fee at http://dhl-fee.net/track or it will be returned.", "e": "spam"}, {"s": "CBI", "n": "+91-ED", "b": "CBI notice: Aapke khilaf money laundering case darj. Digital arrest warrant. Call 9911000000.", "e": "spam"}, {"s": "Bijli", "n": "+91-MSEB", "b": "Aapka bijli connection aaj raat 9:30 baje band ho jayega. Turant call karein: 9876543210", "e": "spam"}, {"s": "UPI", "n": "+91-PSB", "b": "Dear user, your UPI ID expires in 24h. Renew at http://upi-renew.club/verify", "e": "spam"}, {"s": "Secure-login", "n": "+91-WEB", "b": "Your account is compromised. Verify immediately at http://192.168.44.12/secure-login", "e": "spam"}, {"s": "लोटरी", "n": "+91-LTR", "b": "बधाई हो! आपको KBC लॉटरी में ₹25,00,000 का इनाम मिला। http://kbc-lottery.ml/win", "e": "spam"}, {"s": "बिजली", "n": "+91-UP", "b": "प्रिय ग्राहक, आपका बिजली कनेक्शन आज रात 9:30 बजे काट दिया जाएगा। अभी कॉल करें: 9876543210", "e": "spam"}, {"s": "HDFC Bank", "n": "HDFC-BK", "b": "HDFC Bank: Rs.25,000 credited to a/c XX4521 on 02-May. Avl bal: Rs.1,42,356. -HDFC Bank", "e": "ham"}, {"s": "OTP", "n": "SMS-SBI", "b": "Your OTP for SBI Net Banking login is 483921. Valid for 10 minutes. Do not share. -SBI", "e": "ham"}, {"s": "Airtel", "n": "AIRTEL", "b": "Airtel Thanks! Your recharge of Rs.239 is successful. Validity: 28 days. Data: 1.5GB/day. -Airtel", "e": "ham"}, {"s": "PhonePe", "n": "PHONEPE", "b": "PhonePe: Rs.500 received from Rahul Kumar. UPI Ref: 4893721019.", "e": "ham"}, {"s": "Amazon", "n": "AMAZON", "b": "Your Amazon order #402-9876543 is out for delivery today. Track: amzn.in/track -Amazon", "e": "ham"}, {"s": "IRCTC", "n": "IRCTC", "b": "Your IRCTC ticket PNR 4567891230 is confirmed. Train 12345 on 05-May. Seat: S4/32. -IRCTC", "e": "ham"}, {"s": "Mum", "n": "+91-FAM", "b": "Mom, I landed safely. Will call from the hotel in an hour.", "e": "ham"}, ] SEED_LABELS = [m["n"] for m in SEED_SMS] EXAMPLES = [ "URGENT: Your SBI account has been suspended. Verify KYC at http://sbi-kyc-verify.xyz", "HDFC Bank: Rs.25,000 credited to a/c XX4521. Avl bal: Rs.1,42,356. -HDFC Bank", "Aapka bijli connection aaj raat 9:30 baje band ho jayega. Turant call karein: 9876543210", "CBI notice: Aapke khilaf money laundering case darj. Digital arrest warrant. Call 9911000000.", "PhonePe: Rs.500 received from Rahul Kumar. UPI Ref: 4893721019.", "बधाई हो! आपको KBC लॉटरी में ₹25,00,000 का इनाम मिला। http://kbc-lottery.ml/win", "Your OTP for SBI login is 483921. Valid 10 mins. Do NOT share. -SBI", "Mom, I landed safely. Will call from the hotel in an hour.", ] def esc(x): return str(x).replace("&", "&").replace("<", "<").replace(">", ">") def risk(conf, level=""): if conf >= 0.75 or level == "high": return "hi", "var(--hi)", "High risk", "🔴" if conf >= 0.55 or level == "medium": return "md", "var(--md)", "Medium risk", "🟠" return "lo", "var(--lo)", "Low risk", "🟢" _CACHE, _CACHE_MAX = {}, 256 def _cache_get(key): return _CACHE.get(key) def _cache_put(key, value): if len(_CACHE) >= _CACHE_MAX: _CACHE.clear() _CACHE[key] = value return value def _run(message): key = ("run", message) hit = _cache_get(key) if hit is not None: return hit r = predictor.predict(message) r["_tokens"] = [w for w in str(message).lower().split() if w.isalpha() and len(w) > 3][:14] return _cache_put(key, r) def _explain(message): key = ("ex", message) hit = _cache_get(key) if hit is not None: return hit try: # 8 features keeps the SHAP kernel small — this is the expensive part. out = explainer.explain_text(message, num_features=8) except Exception: out = {"top_spam_words": [], "top_ham_words": []} return _cache_put(key, out) # ── renderers ──────────────────────────────────────────────────────────────── SHAP_PENDING = ( '
cybercrime.gov.in · ▸ National helpline 1930Paste any SMS — English, Hindi or Hinglish — and ScamShield returns a verdict, ' 'the exact signals behind it, and word-level SHAP attribution.
XLM-RoBERTa · 9 URL + 8 text signals · Google Safe Browsing '
'· SHAP · threshold 0.55
Spam F1 0.94 · multilingual · encrypted Android client '
'ships as an APK on the same API
READ_SMS), encrypts every request '
'with AES-256-CBC, and raises a notification when a message is flagged.