# ============================================================ # TruthLens — AI Fake News Detector (High Accuracy & Simplified) # HuggingFace Spaces compatible (Gradio native components) # Stack: HuggingFace Transformers + DuckDuckGo Search + Gradio # ============================================================ import re import json from urllib.parse import urlparse import numpy as np import torch import gradio as gr from PIL import Image, ImageEnhance, ImageOps import pytesseract from transformers import ( pipeline, AutoTokenizer, AutoModelForSeq2SeqLM, ) from duckduckgo_search import DDGS # ───────────────────────────────────────── # 1. LOAD MODELS # ───────────────────────────────────────── print("Loading summariser (facebook/bart-large-cnn)...") _SUM_NAME = "facebook/bart-large-cnn" _sum_tokenizer = AutoTokenizer.from_pretrained(_SUM_NAME) _sum_model = AutoModelForSeq2SeqLM.from_pretrained(_SUM_NAME) _sum_model.eval() print("Loading fake-news classifier...") classifier = pipeline( "text-classification", model="hamzab/roberta-fake-news-classification", truncation=True, ) print("Loading zero-shot classifier (facebook/bart-large-mnli)...") zs = pipeline( "zero-shot-classification", model="facebook/bart-large-mnli", ) print("All models ready.") # ───────────────────────────────────────── # 2. DOMAIN CREDIBILITY REGISTRY # ───────────────────────────────────────── TRUSTED = { "reuters.com": 10, "apnews.com": 10, "bbc.com": 9, "bbc.co.uk": 9, "snopes.com": 10, "factcheck.org": 10, "politifact.com": 10, "fullfact.org": 9, "afp.com": 9, "nytimes.com": 8, "washingtonpost.com": 8, "theguardian.com": 8, "npr.org": 8, "pbs.org": 8, "theatlantic.com": 7, "economist.com": 8, "ft.com": 8, "bloomberg.com": 7, "time.com": 7, "forbes.com": 6, "cnn.com": 6, "nbcnews.com": 6, "abcnews.go.com": 6, "cbsnews.com": 6, "ndtv.com": 6, "thehindu.com": 7, "hindustantimes.com": 6, "indiatoday.in": 6, "wikipedia.org": 5, } SATIRE = ["theonion.com","babylonbee.com","clickhole.com", "worldnewsdailyreport.com","waterfordwhispersnews.com"] DISREPUTABLE = ["infowars.com","naturalnews.com","beforeitsnews.com"] def domain_score(url): try: d = urlparse(url).netloc.lower().replace("www.", "") except Exception: return 0, "Unknown" for bad in SATIRE: if bad in d: return -8, "Satire" for bad in DISREPUTABLE: if bad in d: return -6, "Disreputable" for good, s in TRUSTED.items(): if d.endswith(good) or good in d: if s >= 9: return s, "Premier" if s >= 7: return s, "Credible" return s, "Mainstream" return 2, "Unknown" # ───────────────────────────────────────── # 3. TEXT UTILITIES # ───────────────────────────────────────── STOP = { "the","a","an","is","was","are","were","in","on","at","to","of", "and","or","for","with","that","this","it","as","by","from", "has","have","had","be","been","not","but","so","do","did","will", } def clean_text(text): text = re.sub(r'http\S+|www\.\S+|\S+@\S+', '', text) text = re.sub(r'[^a-zA-Z0-9\s.,!?\'\"%-]', ' ', text) return re.sub(r'\s+', ' ', text).strip() def build_query(text): # ACCURACY FIX 1: Removed exact quotes so search works better words = re.findall(r'\b[a-zA-Z0-9]{3,}\b', text) meaningful = [w for w in words if w.lower() not in STOP] core = " ".join(meaningful[:12]) return f"{core} news" def ocr_image(image): if image is None: return "" if not isinstance(image, Image.Image): image = Image.fromarray(np.uint8(image)) image = ImageOps.grayscale(image) image = ImageEnhance.Contrast(image).enhance(2.5) image = image.point(lambda x: 0 if x < 140 else 255) return clean_text(pytesseract.image_to_string(image, config='--oem 3 --psm 6')) # ───────────────────────────────────────── # 4. SUMMARISE # ───────────────────────────────────────── def summarise(text): words = text.split() if len(words) < 30: return text chunk = " ".join(words[:800]) try: inputs = _sum_tokenizer(chunk, return_tensors="pt", max_length=1024, truncation=True) with torch.no_grad(): ids = _sum_model.generate( inputs["input_ids"], max_new_tokens=80, min_length=25, num_beams=4, early_stopping=True, ) return _sum_tokenizer.decode(ids[0], skip_special_tokens=True) except Exception: return " ".join(words[:40]) # ───────────────────────────────────────── # 5. WEB SEARCH # ───────────────────────────────────────── def web_search(summary_text): query = build_query(summary_text) try: raw = list(DDGS().text(query, max_results=7)) except Exception as e: print(f"DuckDuckGo API Error: {e}") return [], f"Search error: {e}", 0 sources, total = [], 0 for r in raw: link = r.get("href", "") sc, tier = domain_score(link) total += max(0, sc) sources.append({ "title": r.get("title", "—"), "body": r.get("body", "")[:240], "link": link, "score": sc, "tier": tier, }) sources.sort(key=lambda x: x["score"], reverse=True) return sources, query, total # ───────────────────────────────────────── # 6. VERDICT ENGINE # ───────────────────────────────────────── def verdict_engine(text, web_total, sources): score, signals = 50, [] # --- 1. AI Classifier --- try: r = classifier(text[:512])[0] lbl = r["label"].upper() conf = round(r["score"] * 100, 1) if lbl in ["REAL", "TRUE"]: pts = round(conf / 100 * 40) score += pts signals.append(("🤖 AI Classifier", f"REAL ({conf}%)", f"+{pts}", "pos")) else: pts = round(conf / 100 * 40) score -= pts signals.append(("🤖 AI Classifier", f"FAKE ({conf}%)", f"-{pts}", "neg")) except Exception: signals.append(("🤖 AI Classifier", "Skipped", "±0", "neu")) # --- 2. Zero-Shot NLI --- try: zr = zs(text[:512], ["credible news", "misinformation", "satire"]) top = zr["labels"][0] zcon = round(zr["scores"][0] * 100, 1) if top == "credible news": score += 15 signals.append(("🧠 Zero-Shot NLI", f"Credible ({zcon}%)", "+15", "pos")) elif top == "misinformation": score -= 15 signals.append(("🧠 Zero-Shot NLI", f"Misinformation ({zcon}%)", "-15", "neg")) else: score -= 8 signals.append(("🧠 Zero-Shot NLI", f"Satire ({zcon}%)", "-8", "neg")) except Exception: signals.append(("🧠 Zero-Shot NLI", "Skipped", "±0", "neu")) # --- 3. Web Coverage --- if not sources: # ACCURACY FIX 2: Lowered penalty from -15 to -5 when API fails score -= 5 signals.append(("🌐 Web Coverage", "No sources found", "-5", "neg")) elif web_total >= 20: pts = min(35, web_total * 2) score += pts signals.append(("🌐 Web Coverage", f"Strong ({web_total} pts)", f"+{pts}", "pos")) elif web_total >= 8: score += 10 signals.append(("🌐 Web Coverage", f"Moderate ({web_total} pts)", "+10", "pos")) else: score -= 10 signals.append(("🌐 Web Coverage", f"Weak ({web_total} pts)", "-10", "neg")) score = max(0, min(100, score)) if score >= 70: tag, cls = "✅ LIKELY REAL", "real" elif score >= 45: tag, cls = "⚠️ UNCERTAIN", "uncertain" else: tag, cls = "🚨 LIKELY FAKE", "fake" return score, tag, cls, signals # ───────────────────────────────────────── # 7. FORMAT OUTPUT AS HTML (SIMPLIFIED) # ───────────────────────────────────────── def build_html(score, tag, cls, signals, summary, query, sources): cls_colors = { "real": ("#2ecc71", "rgba(46,204,113,0.08)"), "fake": ("#e05252", "rgba(224,82,82,0.08)"), "uncertain": ("#d4a843", "rgba(212,168,67,0.08)"), } color, bg = cls_colors.get(cls, cls_colors["uncertain"]) desc = { "real": "Content appears credible based on AI analysis.", "fake": "Significant misinformation indicators detected.", "uncertain": "Evidence is mixed — manual verification recommended." }.get(cls, "") html = f"""
{tag}
{desc}
""" return html # ───────────────────────────────────────── # 8. MAIN PIPELINE # ───────────────────────────────────────── def analyse(text, image): ocr_text = "" if image is not None: ocr_text = ocr_image(image) if len(ocr_text.split()) < 3: ocr_text = "" provided_text = clean_text(text or "") raw = "" if ocr_text and provided_text: raw = f"{ocr_text}. {provided_text}" elif ocr_text: raw = ocr_text else: raw = provided_text if len(raw.split()) < 3: return build_error_html("Input too short. Please provide a headline or sentence.") summary = summarise(raw) sources, query, wt = web_search(summary) score, tag, cls, signals = verdict_engine(raw, wt, sources) return build_html(score, tag, cls, signals, summary, query, sources) def build_error_html(msg): return f"""
⚠️  {msg}
""" # ───────────────────────────────────────── # 9. GRADIO UI # ───────────────────────────────────────── GOOGLE_FONTS = """@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Mono:wght@400;500&family=Lora:ital,wght@0,400;1,400&display=swap');""" CUSTOM_CSS = GOOGLE_FONTS + """ /* ── RESET & BASE ── */ *, *::before, *::after { box-sizing: border-box; } body, .gradio-container, #root { background: #0a0a0e !important; font-family: 'Lora', Georgia, serif !important; color: #e8e2d9 !important; min-height: 100vh; } .gradio-container { max-width: 1100px !important; margin: 0 auto !important; padding: 0 !important; } /* ── HEADER ── */ #tl-header-html { text-align: center; padding: 52px 24px 36px; border-bottom: 1px solid #1e1e26; background: linear-gradient(180deg, #050508 0%, #0a0a0e 100%); margin-bottom: 0; } /* ── PANELS / BOXES ── */ .tl-input-col, .tl-result-col { background: #111116; border: 1px solid #1e1e26; border-radius: 10px; padding: 26px 28px; } /* ── GRADIO OVERRIDES ── */ .gr-form label, label.svelte-1b6s6s, .block > label { font-family: 'DM Mono', monospace !important; font-size: 9px !important; letter-spacing: .3em !important; text-transform: uppercase !important; color: #4a4a56 !important; margin-bottom: 10px !important; } textarea, .gr-textbox textarea, input[type="text"] { background: #141418 !important; border: 1px solid #252530 !important; border-radius: 7px !important; color: #e8e2d9 !important; font-family: 'Lora', serif !important; font-size: 14px !important; line-height: 1.75 !important; padding: 14px 16px !important; transition: border-color .2s, box-shadow .2s !important; resize: vertical !important; } textarea:focus, input[type="text"]:focus { border-color: #3a3a50 !important; box-shadow: 0 0 0 3px rgba(224,82,82,0.07) !important; outline: none !important; } textarea::placeholder { color: #3a3a44 !important; } .gr-image, .image-container, [data-testid="image"] { background: #141418 !important; border: 1.5px dashed #252530 !important; border-radius: 7px !important; transition: border-color .2s !important; } .gr-image:hover { border-color: #e05252 !important; } #tl-analyse-btn, button#tl-analyse-btn { background: #e05252 !important; color: white !important; font-family: 'Syne', sans-serif !important; font-weight: 700 !important; font-size: 13px !important; letter-spacing: .18em !important; text-transform: uppercase !important; border: none !important; border-radius: 7px !important; padding: 15px 24px !important; cursor: pointer !important; transition: background .2s, box-shadow .2s !important; width: 100% !important; margin-top: 6px !important; } #tl-analyse-btn:hover { background: #c94444 !important; box-shadow: 0 0 28px rgba(224,82,82,0.35) !important; } #tl-clear-btn { background: transparent !important; color: #4a4a56 !important; font-family: 'DM Mono', monospace !important; font-size: 11px !important; letter-spacing: .12em !important; text-transform: uppercase !important; border: 1px solid #252530 !important; border-radius: 7px !important; padding: 10px 24px !important; cursor: pointer !important; transition: all .2s !important; width: 100% !important; } #tl-clear-btn:hover { border-color: #3a3a50 !important; color: #e8e2d9 !important; } .gr-html, [data-testid="html"] { background: transparent !important; border: none !important; padding: 0 !important; } .gr-accordion { background: #111116 !important; border: 1px solid #1e1e26 !important; border-radius: 8px !important; } footer { display: none !important; } .built-with { display: none !important; } #share-btn-container { display: none !important; } .gap-4 { gap: 20px !important; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: #0a0a0e; } ::-webkit-scrollbar-thumb { background: #252530; border-radius: 3px; } """ HEADER_HTML = """
— Powered by AI —
TruthLens
AI-powered misinformation detector
""" EXAMPLES = [ ["Scientists at MIT have discovered that drinking coffee daily can reverse the aging process in humans.", None], ["NASA confirms water ice found on the surface of Mars in a new landmark discovery published in Science journal.", None], ["The government is secretly adding mind-control chemicals to public drinking water to suppress dissent among citizens.", None], ] PLACEHOLDER = ( "Paste a news headline or any claim you want to verify...\n\n" "Tip: You can also upload a screenshot of a news article." ) with gr.Blocks( title="TruthLens — AI Fake News Detector", css=CUSTOM_CSS, theme=gr.themes.Base( primary_hue=gr.themes.colors.red, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Lora"), "Georgia", "serif"], ), ) as app: gr.HTML(HEADER_HTML) with gr.Row(equal_height=False): with gr.Column(scale=4, min_width=320): gr.HTML("""
News Text or Headline
""") text_input = gr.Textbox( lines=7, max_lines=16, placeholder=PLACEHOLDER, show_label=False, container=False, ) gr.HTML("""
Or Upload Screenshot (OCR)
""") image_input = gr.Image( type="pil", label="Upload screenshot", show_label=False, container=False, height=180, ) gr.HTML('
') analyse_btn = gr.Button( "→ Analyse Claim", variant="primary", elem_id="tl-analyse-btn", ) clear_btn = gr.Button( "✕ Clear", variant="secondary", elem_id="tl-clear-btn", ) gr.HTML("""
⚠️ AI verdict is advisory only
""") with gr.Column(scale=6, min_width=400): gr.HTML("""
Analysis Result
""") result_output = gr.HTML( value="""
🔍
Paste a claim and press Analyse
""", show_label=False, container=False, ) gr.HTML("""
Try These Examples
""") gr.Examples( examples=EXAMPLES, inputs=[text_input, image_input], outputs=result_output, fn=analyse, cache_examples=False, label=None, ) analyse_btn.click( fn=analyse, inputs=[text_input, image_input], outputs=result_output, show_progress="minimal", ) def clear_all(): empty = """
🔍
Paste a claim and press Analyse
""" return "", None, empty clear_btn.click( fn=clear_all, inputs=[], outputs=[text_input, image_input, result_output], ) app.load( fn=None, js=""" () => { document.addEventListener('keydown', function(e) { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { const btn = document.getElementById('tl-analyse-btn'); if (btn) btn.click(); } }); } """ ) if __name__ == "__main__": app.launch()