FND / app.py
jk2951's picture
Update app.py
7395f44 verified
Raw
History Blame Contribute Delete
23.6 kB
# ============================================================
# 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"""
<div style="font-family:'Lora',Georgia,serif;color:#e8e2d9;background:#0d0d0f;
border-radius:12px;overflow:hidden;border:1px solid #1e1e26;
padding:40px 30px; text-align:center;">
<div style="background:{bg}; border:2px solid {color}; border-radius:10px;
padding:30px; display:inline-block; min-width:300px;">
<div style="font-family:'Syne',sans-serif;font-weight:800;font-size:2.8rem;
letter-spacing:-.03em;color:{color}; line-height:1;">
{tag}
</div>
<div style="border:none;border-top:1px solid #1e1e26;width:80px;margin:20px auto;"></div>
<div style="font-family:'Lora',serif;font-size:16px;color:#a09898;
line-height:1.6; max-width:400px; margin:0 auto;">
{desc}
</div>
</div>
</div>
"""
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"""<div style="background:rgba(224,82,82,0.07);border:1px solid rgba(224,82,82,0.25);
border-radius:8px;padding:20px 22px;color:#e07878;
font-family:'DM Mono',monospace;font-size:13px; text-align:center;">
⚠️ &nbsp;{msg}
</div>"""
# ─────────────────────────────────────────
# 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 = """
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="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" rel="stylesheet">
<div id="tl-header-html" style="
text-align:center;
padding:52px 24px 36px;
border-bottom:1px solid #1e1e26;
background:linear-gradient(180deg,#050508 0%,#0a0a0e 100%);
position:relative;overflow:hidden;">
<div style="position:absolute;top:-80px;left:50%;transform:translateX(-50%);
width:500px;height:220px;
background:radial-gradient(ellipse,rgba(224,82,82,0.06) 0%,transparent 70%);
pointer-events:none;"></div>
<div style="font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.5em;
color:#3a3a44;text-transform:uppercase;margin-bottom:14px;">
β€” Powered by AI β€”
</div>
<div style="font-family:'Syne',sans-serif;font-size:clamp(2.4rem,5vw,4rem);
font-weight:800;letter-spacing:-.03em;line-height:1;color:#e8e2d9;">
Truth<span style="color:#e05252;">Lens</span>
</div>
<div style="border:none;border-top:1px solid #1e1e26;width:56px;margin:16px auto 12px;"></div>
<div style="font-style:italic;color:#4a4a56;font-size:15px;font-family:'Lora',serif;">
AI-powered misinformation detector
</div>
</div>
"""
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("""
<div style="font-family:'DM Mono',monospace;font-size:9px;letter-spacing:.3em;
text-transform:uppercase;color:#4a4a56;margin:20px 0 10px;
display:flex;align-items:center;gap:10px;">
News Text or Headline
<span style="flex:1;height:1px;background:#1e1e26;display:block;"></span>
</div>""")
text_input = gr.Textbox(
lines=7,
max_lines=16,
placeholder=PLACEHOLDER,
show_label=False,
container=False,
)
gr.HTML("""
<div style="font-family:'DM Mono',monospace;font-size:9px;letter-spacing:.3em;
text-transform:uppercase;color:#4a4a56;margin:18px 0 10px;
display:flex;align-items:center;gap:10px;">
Or Upload Screenshot (OCR)
<span style="flex:1;height:1px;background:#1e1e26;display:block;"></span>
</div>""")
image_input = gr.Image(
type="pil",
label="Upload screenshot",
show_label=False,
container=False,
height=180,
)
gr.HTML('<div style="height:12px;"></div>')
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("""
<div style="margin-top:20px;padding:10px 14px;background:#0e0e12;
border:1px solid #1e1e26;border-radius:7px;
font-family:'DM Mono',monospace;font-size:10px;color:#3a3a44;
text-align:center;letter-spacing:.06em;">
⚠️ AI verdict is advisory only
</div>""")
with gr.Column(scale=6, min_width=400):
gr.HTML("""
<div style="font-family:'DM Mono',monospace;font-size:9px;letter-spacing:.3em;
text-transform:uppercase;color:#4a4a56;margin:20px 0 10px;
display:flex;align-items:center;gap:10px;">
Analysis Result
<span style="flex:1;height:1px;background:#1e1e26;display:block;"></span>
</div>""")
result_output = gr.HTML(
value="""
<div style="text-align:center;padding:72px 24px;color:#2a2a34;
border:1px dashed #1e1e26;border-radius:10px;background:#0e0e12;">
<div style="font-size:3rem;margin-bottom:16px;opacity:.3;">πŸ”</div>
<div style="font-family:'DM Mono',monospace;font-size:12px;letter-spacing:.1em;
text-transform:uppercase;">
Paste a claim and press Analyse
</div>
</div>""",
show_label=False,
container=False,
)
gr.HTML("""
<div style="font-family:'DM Mono',monospace;font-size:9px;letter-spacing:.3em;
text-transform:uppercase;color:#4a4a56;margin:28px 0 12px;
display:flex;align-items:center;gap:10px;">
Try These Examples
<span style="flex:1;height:1px;background:#1e1e26;display:block;"></span>
</div>""")
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 = """
<div style="text-align:center;padding:72px 24px;color:#2a2a34;
border:1px dashed #1e1e26;border-radius:10px;background:#0e0e12;">
<div style="font-size:3rem;margin-bottom:16px;opacity:.3;">πŸ”</div>
<div style="font-family:'DM Mono',monospace;font-size:12px;letter-spacing:.1em;
text-transform:uppercase;">
Paste a claim and press Analyse
</div>
</div>"""
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()