|
|
| |
| |
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| |
| 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.") |
|
|
| |
| |
| |
| 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" |
|
|
| |
| |
| |
| 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): |
| |
| 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')) |
|
|
| |
| |
| |
| 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]) |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| def verdict_engine(text, web_total, sources): |
| score, signals = 50, [] |
|
|
| |
| 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")) |
|
|
| |
| 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")) |
|
|
| |
| if not sources: |
| |
| 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 |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| 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;"> |
| β οΈ {msg} |
| </div>""" |
|
|
| |
| |
| |
| 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() |
|
|