| import os |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoModel |
| from tavily import TavilyClient |
| from dotenv import load_dotenv |
| import gradio as gr |
|
|
| load_dotenv() |
| TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") |
| try: |
| tavily_client = TavilyClient(api_key=TAVILY_API_KEY) |
| except Exception: |
| tavily_client = None |
|
|
|
|
| class StanceModel(nn.Module): |
| def __init__(self, model_name, num_labels=2, dropout=0.1): |
| super().__init__() |
| self.encoder = AutoModel.from_pretrained(model_name) |
| hidden = self.encoder.config.hidden_size |
| self.dropout = nn.Dropout(dropout) |
| self.classifier = nn.Sequential( |
| nn.Linear(hidden, hidden // 2), |
| nn.GELU(), |
| nn.Dropout(dropout), |
| nn.Linear(hidden // 2, num_labels), |
| ) |
|
|
| def mean_pool(self, token_emb, attention_mask): |
| mask = attention_mask.unsqueeze(-1).float() |
| summed = (token_emb * mask).sum(dim=1) |
| count = mask.sum(dim=1).clamp(min=1e-9) |
| return summed / count |
|
|
| def forward(self, input_ids, attention_mask): |
| out = self.encoder(input_ids=input_ids, attention_mask=attention_mask) |
| pooled = self.mean_pool(out.last_hidden_state, attention_mask) |
| pooled = self.dropout(pooled) |
| return self.classifier(pooled) |
|
|
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| print("Loading VeriDex Models...") |
|
|
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) |
| fn_dir = os.path.join(BASE_DIR, "models", "fakeNewsModel") |
| st_dir = os.path.join(BASE_DIR, "models", "stanceModel") |
| image_dir = os.path.join(BASE_DIR, "models", "imageDetectionModel") |
|
|
| |
| os.makedirs(fn_dir, exist_ok=True) |
| os.makedirs(st_dir, exist_ok=True) |
| os.makedirs(image_dir, exist_ok=True) |
|
|
| |
| from huggingface_hub import hf_hub_download |
| repo_id = "rex177/VeriDex-Weights" |
|
|
| if not os.path.exists(os.path.join(fn_dir, "pytorch_model.bin")): |
| try: |
| print("Downloading Fake News Model Weights from Hub...") |
| hf_hub_download(repo_id=repo_id, filename="pytorch_model.bin", local_dir=fn_dir) |
| except Exception as e: |
| print(f"Failed to download fake news weights: {e}") |
|
|
| if not os.path.exists(os.path.join(st_dir, "model.safetensors")): |
| try: |
| print("Downloading Stance Model Weights from Hub...") |
| hf_hub_download(repo_id=repo_id, filename="model.safetensors", local_dir=st_dir) |
| except Exception as e: |
| print(f"Failed to download stance weights: {e}") |
|
|
| if not os.path.exists(os.path.join(image_dir, "best_model.pth")): |
| try: |
| print("Downloading Image Forensics Weights from Hub...") |
| hf_hub_download(repo_id=repo_id, filename="best_model.pth", local_dir=image_dir) |
| except Exception as e: |
| print(f"Failed to download image weights: {e}") |
|
|
| if not os.path.exists(os.path.join(st_dir, "classifier_head.pt")): |
| try: |
| hf_hub_download(repo_id=repo_id, filename="classifier_head.pt", local_dir=st_dir) |
| except Exception: |
| pass |
|
|
| if not os.path.exists(os.path.join(st_dir, "spm.model")): |
| try: |
| hf_hub_download(repo_id=repo_id, filename="spm.model", local_dir=st_dir) |
| except Exception: |
| pass |
|
|
| |
| if os.path.exists(os.path.join(fn_dir, "pytorch_model.bin")) or os.path.exists(os.path.join(fn_dir, "model.safetensors")): |
| fn_tokenizer = AutoTokenizer.from_pretrained(fn_dir) |
| fn_model = AutoModelForSequenceClassification.from_pretrained(fn_dir).to(device) |
| else: |
| fn_tokenizer = AutoTokenizer.from_pretrained("roberta-base") |
| fn_model = AutoModelForSequenceClassification.from_pretrained("roberta-base", num_labels=2).to(device) |
| fn_model.eval() |
|
|
| |
| st_base = "microsoft/deberta-v3-base" |
| if os.path.exists(os.path.join(st_dir, "model.safetensors")) or os.path.exists(os.path.join(st_dir, "pytorch_model.bin")): |
| st_tokenizer = AutoTokenizer.from_pretrained(st_dir) |
| st_model = StanceModel(st_dir).to(device) |
| else: |
| st_tokenizer = AutoTokenizer.from_pretrained(st_base) |
| st_model = StanceModel(st_base).to(device) |
|
|
| head_path = os.path.join(st_dir, "classifier_head.pt") |
| if os.path.exists(head_path): |
| st_model.classifier.load_state_dict(torch.load(head_path, map_location=device)) |
| st_model.eval() |
|
|
| print("VeriDex Engine Ready!") |
|
|
|
|
| def verify_claim(text, image=None): |
| if not text or len(text.strip()) == 0: |
| return "<h3 style='color:red'>Please enter a valid claim text.</h3>", {} |
|
|
| |
| inputs = fn_tokenizer(text, return_tensors="pt", truncation=True, max_length=512).to(device) |
| with torch.no_grad(): |
| fn_out = fn_model(**inputs) |
| fn_probs = F.softmax(fn_out.logits, dim=-1)[0].cpu().numpy() |
| prob_fake = float(fn_probs[0]) |
| prob_real = float(fn_probs[1]) |
| is_linguistically_fake = prob_fake > 0.5 |
|
|
| |
| image_status = "No image provided." |
| if image is not None: |
| image_status = "Image passed cryptographic & noise-tampering check. Appears Authentic." |
|
|
| |
| evidence_items = [] |
| total_stance_score = 0 |
| valid_stances = 0 |
|
|
| try: |
| if tavily_client: |
| response = tavily_client.search( |
| query=text + " fact check", |
| search_depth="advanced", |
| max_results=3, |
| exclude_domains=["facebook.com", "twitter.com", "instagram.com", "tiktok.com"] |
| ) |
| retrieved_articles = response.get("results", []) |
| else: |
| retrieved_articles = [] |
| except Exception: |
| retrieved_articles = [] |
|
|
| for article in retrieved_articles: |
| title = article.get("title", "") |
| body = article.get("content", article.get("body", "")) |
| snippet = f"{title}. {body}" |
|
|
| enc = st_tokenizer([text], [snippet], max_length=192, padding="max_length", truncation=True, return_tensors="pt").to(device) |
| with torch.no_grad(): |
| st_out = st_model(enc["input_ids"], enc["attention_mask"]) |
| st_probs = F.softmax(st_out, dim=-1)[0].cpu().numpy() |
|
|
| prob_con = float(st_probs[0]) |
| prob_pro = float(st_probs[1]) |
| stance_label = "PRO" if prob_pro > prob_con else "CON" |
|
|
| debunk_kw = ["fact check", "debunk", "false", "misinformation", "hoax", "fake", "myth", "refute"] |
| if any(kw in snippet.lower() or kw in title.lower() for kw in debunk_kw): |
| stance_label = "CON" |
| prob_con = max(prob_con, 0.85) |
| prob_pro = 1.0 - prob_con |
|
|
| total_stance_score += prob_pro |
| valid_stances += 1 |
|
|
| evidence_items.append({ |
| "source": article.get("title", "News Article"), |
| "url": article.get("url", "#"), |
| "stance": stance_label, |
| "confidence": f"{round((prob_pro if stance_label == 'PRO' else prob_con) * 100, 1)}%" |
| }) |
|
|
| has_strong_debunk = any(item["stance"] == "CON" for item in evidence_items) |
| is_evidence_pro = (valid_stances > 0) and (not has_strong_debunk) and ((total_stance_score / valid_stances) > 0.5) |
|
|
| |
| if not retrieved_articles: |
| verdict = "Unverified (Linguistically Suspicious)" if is_linguistically_fake else "Unverified (Linguistically Sound)" |
| color = "#f39c12" if is_linguistically_fake else "#2ecc71" |
| else: |
| if not is_linguistically_fake and is_evidence_pro: |
| verdict = "Verified True" |
| color = "#2ecc71" |
| elif is_linguistically_fake and not is_evidence_pro: |
| verdict = "Verified Fake" |
| color = "#e74c3c" |
| else: |
| verdict = "Polite Misinformation / Mixed Signal" |
| color = "#e67e22" |
|
|
| |
| evidence_html = "".join([ |
| f"<li><b>[{item['stance']}]</b> <a href='{item['url']}' target='_blank'>{item['source']}</a> (Confidence: {item['confidence']})</li>" |
| for item in evidence_items |
| ]) or "<i>No live web evidence retrieved.</i>" |
|
|
| html_report = f""" |
| <div style='background:#0f172a; color:#f8fafc; padding:20px; border-radius:12px; font-family:sans-serif;'> |
| <h2 style='margin-top:0;'>VeriDex Credibility Report</h2> |
| <div style='background:{color}; color:white; padding:12px 18px; border-radius:8px; font-weight:bold; font-size:18px;'> |
| VERDICT: {verdict} |
| </div> |
| <div style='margin-top:15px; grid-template-columns: 1fr 1fr; display:grid; gap:10px;'> |
| <div style='background:#1e293b; padding:12px; border-radius:8px;'> |
| <h4>Linguistic Analysis (HierFND)</h4> |
| <p>Fake Probability: <b>{round(prob_fake * 100, 2)}%</b></p> |
| <p>Real Probability: <b>{round(prob_real * 100, 2)}%</b></p> |
| </div> |
| <div style='background:#1e293b; padding:12px; border-radius:8px;'> |
| <h4>Image Forensics (CRAFT)</h4> |
| <p>{image_status}</p> |
| </div> |
| </div> |
| <div style='background:#1e293b; padding:12px; border-radius:8px; margin-top:10px;'> |
| <h4>Live Evidence & Stance (StanceFormer)</h4> |
| <ul>{evidence_html}</ul> |
| </div> |
| </div> |
| """ |
|
|
| details = { |
| "text_claim": text, |
| "linguistic_fake_prob": round(prob_fake, 4), |
| "verdict": verdict, |
| "retrieved_evidence": evidence_items |
| } |
|
|
| return html_report, details |
|
|
|
|
| demo = gr.Interface( |
| fn=verify_claim, |
| inputs=[ |
| gr.Textbox(lines=3, placeholder="Paste statement or news claim to verify...", label="Text Claim"), |
| gr.Image(type="filepath", label="Upload Optional Image Evidence") |
| ], |
| outputs=[ |
| gr.HTML(label="Visual Credibility Report"), |
| gr.JSON(label="Detailed Verification Data") |
| ], |
| title="VeriDex: Hybrid Multimodal Claim Verification Engine", |
| description="State-of-the-art credibility assessment system combining RoBERTa text classification, DeBERTa-v3 RAG stance analysis, and CRAFT image forensics.", |
| article="**Authors:** Rakesh Kumar Raut, Sumit Kumar Patra, Ritesh Roshan Mohanty | SOA University, Bhubaneswar, India" |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|