Spaces:
Sleeping
Sleeping
| import sqlite3 | |
| from datetime import datetime | |
| from pathlib import Path | |
| import gradio as gr | |
| from ml_utils import ScamDetectionService | |
| detector = ScamDetectionService() | |
| # ββ DB ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DB_PATH = Path("feedback.db") | |
| def init_db(): | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.execute(""" | |
| CREATE TABLE IF NOT EXISTS feedback ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| content TEXT NOT NULL, | |
| content_type TEXT NOT NULL, | |
| prediction TEXT NOT NULL, | |
| confidence REAL NOT NULL, | |
| user_agreed INTEGER NOT NULL, | |
| timestamp TEXT NOT NULL | |
| ) | |
| """) | |
| conn.commit() | |
| conn.close() | |
| init_db() | |
| def log_feedback(content, content_type, prediction, confidence, agreed): | |
| try: | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.execute( | |
| "INSERT INTO feedback VALUES (NULL,?,?,?,?,?,?)", | |
| (str(content)[:2000], content_type, prediction, | |
| float(confidence), int(agreed), datetime.utcnow().isoformat()) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| except Exception as e: | |
| print(f"[db] {e}") | |
| def get_stats(): | |
| try: | |
| conn = sqlite3.connect(DB_PATH) | |
| total = conn.execute("SELECT COUNT(*) FROM feedback").fetchone()[0] | |
| agreed = conn.execute("SELECT COUNT(*) FROM feedback WHERE user_agreed=1").fetchone()[0] | |
| conn.close() | |
| return total, (round(agreed / total * 100, 1) if total else 0.0) | |
| except: | |
| return 0, 0.0 | |
| # ββ Styles ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| STYLES = { | |
| "Scam": {"accent": "#E05252", "bg": "#1C0E0E", "border": "#5C1F1F", "badge": "SCAM"}, | |
| "Suspicious": {"accent": "#D4924A", "bg": "#1C1408", "border": "#5C3E10", "badge": "SUSPICIOUS"}, | |
| "Safe": {"accent": "#4CAF7D", "bg": "#0C1C13", "border": "#1A5C35", "badge": "LOOKS SAFE"}, | |
| } | |
| def result_html(s, meta_rows, user_message): | |
| return f""" | |
| <div style=" | |
| background:{s['bg']};border:1px solid {s['border']};border-radius:8px; | |
| padding:20px 24px;font-family:'IBM Plex Mono',monospace;margin-top:4px; | |
| "> | |
| <div style=" | |
| display:inline-block;font-size:0.78rem;font-weight:700;color:{s['accent']}; | |
| letter-spacing:0.08em;text-transform:uppercase; | |
| border:1px solid {s['border']};border-radius:4px; | |
| padding:3px 10px;margin-bottom:14px; | |
| ">{s['badge']}</div> | |
| <div style="color:#c8c8c8;font-size:0.85rem;line-height:1.75;"> | |
| {meta_rows} | |
| <div style="margin-top:12px;padding-top:12px;border-top:1px solid #2a2a2a; | |
| color:#aaa;font-size:0.82rem;line-height:1.7;"> | |
| {user_message} | |
| </div> | |
| </div> | |
| </div>""" | |
| EMPTY = "<div style='color:#666;padding:16px;font-family:\"IBM Plex Mono\",monospace;font-size:0.85rem;'>Enter a value above to analyze.</div>" | |
| SAVED = "<div style='color:#4CAF7D;font-family:\"IBM Plex Mono\",monospace;font-size:0.78rem;padding:6px 0;'>Feedback saved. Thanks.</div>" | |
| # ββ State: last result for feedback ββββββββββββββββββββββββββββββββββββββββββ | |
| # We store last prediction in Gradio State so the feedback buttons can read it. | |
| def analyze_text(text): | |
| if not text or not text.strip(): | |
| return EMPTY, gr.update(visible=False), gr.update(visible=False), {} | |
| r = detector.analyze_text_scam(text) | |
| risk = r['risk_level'] | |
| conf = r['confidence'] | |
| lang = r.get('detected_language', 'en').upper() | |
| msg = r.get('user_message', r['reasoning']) | |
| s = STYLES.get(risk, STYLES["Suspicious"]) | |
| meta = ( | |
| f"<span style='color:#666;'>Confidence</span> {conf:.0%}<br>" | |
| f"<span style='color:#666;'>Language </span> {lang}<br>" | |
| ) | |
| html = result_html(s, meta, msg) | |
| state = {"content": text, "content_type": "text", "prediction": risk, "confidence": conf} | |
| return html, gr.update(visible=True), gr.update(visible=False), state | |
| def analyze_url(url, context): | |
| if not url or not url.strip(): | |
| return EMPTY, gr.update(visible=False), gr.update(visible=False), {} | |
| r = detector.analyze_url_scam(url, context) | |
| risk = r['risk_level'] | |
| conf = r['confidence'] | |
| domain = r['domain'] | |
| msg = r.get('user_message', r['reasoning']) | |
| s = STYLES.get(risk, STYLES["Suspicious"]) | |
| meta = ( | |
| f"<span style='color:#666;'>Confidence</span> {conf:.0%}<br>" | |
| f"<span style='color:#666;'>Domain </span> {domain}<br>" | |
| ) | |
| html = result_html(s, meta, msg) | |
| state = {"content": url, "content_type": "url", "prediction": risk, "confidence": conf} | |
| return html, gr.update(visible=True), gr.update(visible=False), state | |
| def on_yes(state): | |
| if state: | |
| log_feedback(state["content"], state["content_type"], | |
| state["prediction"], state["confidence"], agreed=1) | |
| total, rate = get_stats() | |
| return gr.update(visible=False), gr.update(visible=True), f"{SAVED}<div style='color:#555;font-family:IBM Plex Mono,monospace;font-size:0.72rem;padding:2px 0;'>{total} total Β· {rate}% agreement</div>" | |
| return gr.update(visible=False), gr.update(visible=True), SAVED | |
| def on_no(state): | |
| if state: | |
| log_feedback(state["content"], state["content_type"], | |
| state["prediction"], state["confidence"], agreed=0) | |
| total, rate = get_stats() | |
| return gr.update(visible=False), gr.update(visible=True), f"{SAVED}<div style='color:#555;font-family:IBM Plex Mono,monospace;font-size:0.72rem;padding:2px 0;'>{total} total Β· {rate}% agreement</div>" | |
| return gr.update(visible=False), gr.update(visible=True), SAVED | |
| # ββ CSS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;500&display=swap'); | |
| *, *::before, *::after { box-sizing: border-box; } | |
| body, .gradio-container { | |
| background: #111 !important; | |
| font-family: 'IBM Plex Sans', sans-serif !important; | |
| color: #c8c8c8 !important; | |
| } | |
| .gradio-container { | |
| max-width: 660px !important; | |
| margin: 0 auto !important; | |
| padding: 32px 16px !important; | |
| } | |
| .gr-markdown h1 { | |
| font-family: 'IBM Plex Mono', monospace !important; | |
| font-size: 1.4rem !important; font-weight: 600 !important; | |
| color: #e8e8e8 !important; letter-spacing: -0.01em !important; | |
| margin-bottom: 4px !important; | |
| } | |
| .gr-markdown p { | |
| color: #666 !important; font-size: 0.82rem !important; | |
| font-family: 'IBM Plex Mono', monospace !important; line-height: 1.6 !important; | |
| } | |
| textarea, input[type=text] { | |
| background: #181818 !important; border: 1px solid #2e2e2e !important; | |
| color: #d8d8d8 !important; font-family: 'IBM Plex Sans', sans-serif !important; | |
| font-size: 0.88rem !important; border-radius: 6px !important; | |
| transition: border-color 0.15s ease !important; | |
| } | |
| textarea:focus, input[type=text]:focus { | |
| border-color: #444 !important; box-shadow: none !important; outline: none !important; | |
| } | |
| label span, .gr-form label span { | |
| color: #555 !important; font-size: 0.72rem !important; font-weight: 500 !important; | |
| text-transform: uppercase !important; letter-spacing: 0.07em !important; | |
| font-family: 'IBM Plex Mono', monospace !important; | |
| } | |
| button.primary, .gr-button-primary { | |
| background: #e8e8e8 !important; color: #111 !important; border: none !important; | |
| font-family: 'IBM Plex Mono', monospace !important; font-weight: 600 !important; | |
| font-size: 0.78rem !important; letter-spacing: 0.07em !important; | |
| text-transform: uppercase !important; border-radius: 5px !important; | |
| padding: 10px 22px !important; transition: background 0.12s ease !important; | |
| cursor: pointer !important; | |
| } | |
| button.primary:hover, .gr-button-primary:hover { background: #fff !important; } | |
| /* Feedback buttons */ | |
| .fb-yes { background: #1a2a1a !important; border: 1px solid #2a4a2a !important; | |
| color: #4CAF7D !important; font-size: 0.78rem !important; | |
| padding: 4px 14px !important; border-radius: 4px !important; } | |
| .fb-no { background: #2a1a1a !important; border: 1px solid #4a2a2a !important; | |
| color: #E05252 !important; font-size: 0.78rem !important; | |
| padding: 4px 14px !important; border-radius: 4px !important; } | |
| .tab-nav { border-bottom: 1px solid #252525 !important; margin-bottom: 20px !important; } | |
| .tab-nav button { | |
| font-family: 'IBM Plex Mono', monospace !important; font-size: 0.72rem !important; | |
| color: #555 !important; background: transparent !important; border: none !important; | |
| border-bottom: 2px solid transparent !important; text-transform: uppercase !important; | |
| letter-spacing: 0.07em !important; padding: 8px 16px !important; | |
| cursor: pointer !important; transition: color 0.12s ease !important; | |
| } | |
| .tab-nav button.selected { color: #d8d8d8 !important; border-bottom-color: #d8d8d8 !important; } | |
| .tab-nav button:hover:not(.selected) { color: #999 !important; } | |
| .gr-examples { margin-top: 12px !important; } | |
| .gr-examples table { border: none !important; background: transparent !important; } | |
| .gr-examples td, .gr-examples th { | |
| background: #181818 !important; border: 1px solid #252525 !important; | |
| color: #888 !important; font-size: 0.78rem !important; | |
| font-family: 'IBM Plex Mono', monospace !important; | |
| padding: 6px 12px !important; cursor: pointer !important; | |
| transition: background 0.1s ease !important; | |
| } | |
| .gr-examples tr:hover td { background: #202020 !important; color: #bbb !important; } | |
| .gr-form, .gr-box, .gr-block, .gr-panel { | |
| background: transparent !important; border: none !important; box-shadow: none !important; | |
| } | |
| .tabitem { padding: 0 !important; } | |
| hr { border-color: #222 !important; margin: 24px 0 !important; } | |
| """ | |
| # ββ UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(css=css, title="Scam Detector") as demo: | |
| gr.Markdown("# Scam Detector") | |
| gr.Markdown("Paste a suspicious message or URL. Results are flagged as Safe, Suspicious, or Scam.") | |
| with gr.Tab("Text / SMS"): | |
| text_state = gr.State({}) | |
| text_input = gr.Textbox( | |
| label="Message", | |
| placeholder="Hi, this is from your bank's fraud prevention team...", | |
| lines=4 | |
| ) | |
| text_btn = gr.Button("Analyze", variant="primary") | |
| text_out = gr.HTML(EMPTY) | |
| with gr.Row(visible=False) as text_fb_row: | |
| gr.HTML("<span style='font-family:IBM Plex Mono,monospace;font-size:0.75rem;color:#555;'>Was this correct?</span>") | |
| text_yes = gr.Button("Yes", elem_classes=["fb-yes"]) | |
| text_no = gr.Button("No", elem_classes=["fb-no"]) | |
| text_fb_msg = gr.HTML(visible=False) | |
| text_btn.click( | |
| analyze_text, | |
| inputs=text_input, | |
| outputs=[text_out, text_fb_row, text_fb_msg, text_state] | |
| ) | |
| text_yes.click(on_yes, inputs=text_state, outputs=[text_fb_row, text_fb_msg, text_fb_msg]) | |
| text_no.click( on_no, inputs=text_state, outputs=[text_fb_row, text_fb_msg, text_fb_msg]) | |
| gr.Examples( | |
| examples=[ | |
| ["CONGRATULATIONS! You've WON 1000! Click here to claim NOW!"], | |
| ["Hi, this is Rahul from HDFC fraud monitoring. We noticed a Rs.18,420 charge. Confirm here: https://hdfc-secureverify.co"], | |
| ["Your KYC is expiring in 24 hours. Update now to avoid account suspension."], | |
| ["Your Aadhaar is being used to open bank accounts. Immediate verification required."], | |
| ["Amazon: Your package will arrive tomorrow between 2-5 PM."], | |
| ["Hey, want to grab coffee tomorrow at 3pm?"], | |
| ], | |
| inputs=text_input, | |
| label="Try these examples" | |
| ) | |
| with gr.Tab("URL / Link"): | |
| url_state = gr.State({}) | |
| url_input = gr.Textbox(label="URL", placeholder="http://paypa1-secure.tk/verify") | |
| ctx_input = gr.Textbox( | |
| label="Message context (optional)", | |
| placeholder="Your account has been suspended. Verify now.", | |
| lines=2 | |
| ) | |
| url_btn = gr.Button("Analyze", variant="primary") | |
| url_out = gr.HTML(EMPTY) | |
| with gr.Row(visible=False) as url_fb_row: | |
| gr.HTML("<span style='font-family:IBM Plex Mono,monospace;font-size:0.75rem;color:#555;'>Was this correct?</span>") | |
| url_yes = gr.Button("Yes", elem_classes=["fb-yes"]) | |
| url_no = gr.Button("No", elem_classes=["fb-no"]) | |
| url_fb_msg = gr.HTML(visible=False) | |
| url_btn.click( | |
| analyze_url, | |
| inputs=[url_input, ctx_input], | |
| outputs=[url_out, url_fb_row, url_fb_msg, url_state] | |
| ) | |
| url_yes.click(on_yes, inputs=url_state, outputs=[url_fb_row, url_fb_msg, url_fb_msg]) | |
| url_no.click( on_no, inputs=url_state, outputs=[url_fb_row, url_fb_msg, url_fb_msg]) | |
| gr.Examples( | |
| examples=[ | |
| ["http://paypa1-secure.tk/verify", "Your account has been suspended"], | |
| ["https://netflix-payment-failed-verify-account.info", ""], | |
| ["http://crypto-investment-double-money-fast.site", ""], | |
| ["https://www.google.com", ""], | |
| ["https://bluedart-track-update.net", "Your courier could not be delivered"], | |
| ], | |
| inputs=[url_input, ctx_input], | |
| label="Try these examples" | |
| ) | |
| with gr.Row(): | |
| stats_btn = gr.Button("Show feedback stats", size="sm") | |
| stats_out = gr.HTML() | |
| stats_btn.click( | |
| lambda: (lambda t, r: f"<div style='font-family:IBM Plex Mono,monospace;font-size:0.75rem;color:#555;padding:4px 0;'>{t} feedback logged · {r}% agreement</div>")(*get_stats()), | |
| outputs=stats_out | |
| ) | |
| gr.Markdown( | |
| "TF-IDF + LR (text) Β· 3-model URL ensemble (LR + RF + XGBoost) Β· " | |
| "India-specific dataset (500 msgs + 250 URLs) Β· " | |
| "[GitHub](https://github.com/SD1920/ScamDetector)" | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |