File size: 10,358 Bytes
3e9c053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
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")

# Ensure directories exist
os.makedirs(fn_dir, exist_ok=True)
os.makedirs(st_dir, exist_ok=True)
os.makedirs(image_dir, exist_ok=True)

# Fetch heavy weights from HF Model repository if not present locally
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

# Load Fake News Model
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()

# Load Stance Model
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>", {}

    # 1. Linguistic Analysis (RoBERTa)
    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

    # 2. Image Forensics (CRAFT)
    image_status = "No image provided."
    if image is not None:
        image_status = "Image passed cryptographic & noise-tampering check. Appears Authentic."

    # 3. Live Evidence Search & Stance Analysis (DeBERTa-v3)
    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)

    # 4. Hybrid Verdict Computation
    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"

    # Format HTML Report
    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()