File size: 11,151 Bytes
b758e97
 
8d0a810
62ef4c3
b758e97
bd95f0c
8d0a810
 
043bb4d
 
 
 
 
81324b1
0edb773
81324b1
8d0a810
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7455224
8d0a810
b4e73b5
 
 
8fc155a
 
6271b3d
8fc155a
0edb773
fa9cb33
8d0a810
81324b1
7455224
 
 
8fc155a
 
 
7455224
 
8d0a810
0edb773
 
8d0a810
0edb773
8d0a810
0edb773
8d0a810
 
 
 
 
 
 
 
 
 
81324b1
 
8fc155a
 
81324b1
196ac66
 
 
 
 
 
 
81324b1
 
 
196ac66
 
 
 
 
 
 
 
 
 
 
 
 
 
81324b1
 
b4e73b5
81324b1
 
 
 
 
 
 
 
 
 
 
 
 
b4e73b5
 
b758e97
81324b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196ac66
 
81324b1
 
 
196ac66
 
81324b1
 
 
196ac66
81324b1
196ac66
81324b1
196ac66
 
81324b1
b758e97
8d0a810
b4e73b5
 
 
 
 
 
 
62ef4c3
26212b8
8fc155a
62ef4c3
d83c328
8fc155a
 
26212b8
64156cd
8fc155a
eb9d6db
8fc155a
 
 
62ef4c3
196ac66
62ef4c3
196ac66
cd9a82f
 
196ac66
62ef4c3
196ac66
cd9a82f
62ef4c3
 
 
 
cd9a82f
 
62ef4c3
 
 
 
 
196ac66
26212b8
 
 
 
b4e73b5
 
26212b8
62ef4c3
 
 
 
 
 
 
26212b8
78e26d0
 
 
 
 
 
 
62ef4c3
d83c328
bd95f0c
8d0a810
 
 
 
 
 
 
 
 
 
 
 
 
b758e97
 
 
 
 
64156cd
b758e97
 
b4e73b5
 
 
 
 
 
 
 
 
8d0a810
 
 
b4e73b5
 
 
 
 
 
 
 
 
 
 
 
 
 
81324b1
bd95f0c
b758e97
 
 
0edb773
 
b758e97
bd95f0c
b758e97
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import os
import torch
from torch import nn
import json
import requests
import gradio as gr
from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline, CLIPProcessor, CLIPModel
from collections import OrderedDict
import wikipedia
import wikipediaapi
import re
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from tavily import TavilyClient
from huggingface_hub import InferenceClient, hf_hub_download

class CLIPImageClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.clip = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
        self.classifier = nn.Sequential(
            nn.Linear(self.clip.config.vision_config.hidden_size, 256),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )

    def forward(self, pixel_values):
        feats = self.clip.vision_model(pixel_values=pixel_values).pooler_output
        return self.classifier(feats)

text_classifier = None
image_classifier = None
TAVILY_KEY = None
GOOGLE_KEY = None
HF_TOKEN = None

embed_model = SentenceTransformer("all-MiniLM-L6-v2")
explain_model = "meta-llama/Llama-3.1-8B-Instruct"
text_model = "rajyalakshmijampani/fever_finetuned_deberta"
image_model = "rajyalakshmijampani/finetuned_clip"
wiki = wikipediaapi.Wikipedia(language='en', user_agent='fact-checker/1.0')
image_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

def get_text_classifier():
    global text_classifier
    if text_classifier is None:
        tokenizer = AutoTokenizer.from_pretrained(text_model)
        seq_clf = AutoModelForSequenceClassification.from_pretrained(text_model)
        text_classifier = pipeline("text-classification", model=seq_clf, tokenizer=tokenizer)
    return text_classifier

def get_image_classifier():
    global image_classifier, image_model
    filename = "finetuned_clip.pth"
    if image_classifier is None:
        model_path = hf_hub_download(repo_id=image_model, filename=filename)
        image_classifier = CLIPImageClassifier()
        state = torch.load(model_path, map_location="cpu",weights_only=False)
        clean_state = OrderedDict(
            (k[7:], v) if k.startswith("module.") else (k, v)
            for k, v in state.items()
        )
        image_classifier.load_state_dict(clean_state, strict=False)
        image_classifier.eval()
        return image_classifier
    
    return image_classifier

def _rank_sentences(claim, sentences, top_k=4):
    if not sentences: return []
    emb_c = embed_model.encode([claim])
    emb_s = embed_model.encode(sentences)
    sims = cosine_similarity(emb_c, emb_s)[0]

    claim_tokens = set(re.findall(r'\w+', claim.lower()))
    scored = []
    for s, sim in zip(sentences, sims):
        overlap = len(claim_tokens.intersection(set(re.findall(r'\w+', s.lower()))))
        scored.append((s, sim + 0.01 * overlap))  # small lexical boost
    ranked = [s for s, _ in sorted(scored, key=lambda x: x[1], reverse=True)]
    return ranked[:top_k]

def _split_sentences(text):
    sents = re.split(r'(?<=[.!?])\s+', text)
    clean = []
    for s in sents:
        s = s.strip()
        if 15 < len(s) < 350 and not s.lower().startswith(("see also", "references", "external links")):
            clean.append(s)
    return clean

def _safe_call(func, claim):
    try:
        return func(claim)
    except Exception as e:
        print(f"[WARN] {func.__name__} failed: {e}")
        return []

def _from_google(claim):
    global GOOGLE_KEY
    url = "https://factchecktools.googleapis.com/v1alpha1/claims:search"
    r = requests.get(url, params={"query": claim, "key": GOOGLE_KEY, "pageSize": 2}).json()
    claims = r.get("claims", [])
    evid = []
    for c in claims:
        rev = c.get("claimReview", [])
        if rev:
            rating = rev[0].get("textualRating", "")
            site = rev[0].get("publisher", {}).get("name", "")
            evid.append(f"{site} rated this claim as {rating}.")
    return evid[:3]

def _from_tavily(claim):
    global TAVILY_KEY
    tavily = TavilyClient(api_key=TAVILY_KEY)
    try:
        results = tavily.search(claim).get("results", [])
        sents = []
        for r in results:
            for s in _split_sentences(r.get("content", "")):
                if not any(x in s.lower() for x in ["video game", "film", "fiction"]):
                    sents.append(s)
        return _rank_sentences(claim, sents, 4)
    except Exception:
        return []
    
def _from_wiki(claim):
    try:
        titles = wikipedia.search(claim, results=3)
        sents = []
        for t in titles:
            page = wiki.page(t)
            if not page.exists(): continue
            text = page.text[:5000]  # extend a bit
            for s in _split_sentences(text):
                if not any(x in s.lower() for x in ["video game", "fiction", "film"]):
                    sents.append(s)
        return _rank_sentences(claim, sents, 4)
    except Exception as e:
        print(f"[WARN] _from_wiki failed: {e}")
        return []

def get_evidence_sentences(claim, k=3):
    evid = _safe_call(_from_google, claim)
    if len(evid) >= k: return evid[:k]
    evid += _safe_call(_from_tavily, claim)
    if len(evid) >= k: return evid[:k]
    evid += _safe_call(_from_wiki, claim)
    evid = [e for e in evid if len(e.strip()) > 10]
    return (evid or ["Error: No relevant evidence found."])[:k]

# ---Text Classification Function ---
def classify_text(claim, hf_token, tavily_key, google_key):

    global HF_TOKEN, TAVILY_KEY, GOOGLE_KEY
    HF_TOKEN = hf_token.strip()
    TAVILY_KEY = tavily_key.strip()
    GOOGLE_KEY = google_key.strip()

    claim=claim.lower().strip()
    classifier = get_text_classifier()
    evidences = get_evidence_sentences(claim)    
    evidence_text = " ".join(evidences).lower().strip()

    # Step 1: FEVER classification
    text = f"claim: {claim} evidence: {evidence_text}"
    result = classifier(text, truncation=True, max_length=512, return_all_scores=True)[0]
    top_label = sorted(result, key=lambda x: x["score"], reverse=True)[0]["label"]
    label_str = "REAL" if top_label == "LABEL_0" else "FAKE"
    print(f"[INFO] Model Classified {claim} as {label_str}")

    # Step 2: Mistral explanation generation
    prompt = f"""
            You are a reliable fact-checking assistant.

            User's statement: "{claim}"

            Information you have received (use this for reasoning, but do not mention or list it directly):
            {chr(10).join(f"- {e}" for e in evidences)}

            The system’s current assessment is that the claim is: "{label_str}".

            Now, carefully evaluate the statement and the assessment. You may disagree with the system if the evidences clearly contradict the claim.
            Write your reasoning and return it STRICTLY as a JSON object with the following fields:
            {{
                "verdict": "Real / Fake / Uncertain",
                "explanation": "3–5 natural sentences explaining what makes the claim true or fake or uncertain. 
                                Do NOT mention words like 'evidence', 'sources', or 'provided information'. 
                                Instead, explain the reasoning naturally as if you are telling it from general knowledge.",",
                "confidence": "Low / Medium / High 
                               Decide this depending on how strong the evidences are, how clear the reasoning is,
                               and how certain you are about your verdict."
            }}
            Do NOT include anything outside the JSON. Use plain text, no Markdown. Be concise and to the point.
"""
    messages = [
        {"role": "system", "content": "You are a reliable fact-checking assistant."},
        {"role": "user", "content": prompt},
    ]

    inf_client = InferenceClient(token=HF_TOKEN)
    completion = inf_client.chat_completion( model=explain_model, messages=messages, max_tokens=256, temperature=0.3)
    raw_response = completion.choices[0].message.content.strip()

    try:
        data = json.loads(raw_response)
    except json.JSONDecodeError:
        print("[WARN] Could not parse JSON, returning raw text")
        return raw_response
    
    formatted_output = f"""**Prediction:** The claim is {data.get('verdict', 'N/A')}.

**Explanation:**
{data.get('explanation', 'No explanation available.')}

**Confidence:** {data.get('confidence', 'N/A')}."""  
    
    return formatted_output.strip()


# ---- Image classification Function ----
def classify_image(image):
    global image_processor
    classifier = get_image_classifier()
    try:
        inputs = image_processor(images=image.convert("RGB"), return_tensors="pt")["pixel_values"]
        with torch.no_grad():
            output = classifier(inputs)
        p = output.item()
        label = "Fake" if p > 0.5 else "Real"
        return f"**Prediction:** {label}\n**Confidence score:** {p:.2f}"
    except Exception as e:
        return f"Error: {e}"

# -------------------
# UI Layout (Gradio)
# -------------------
with gr.Blocks() as demo:
    gr.Markdown("# Multimodal Misinformation Detector")

    with gr.Tab("Text Detector"):
        with gr.Row(): 
            with gr.Column(scale=3): #  Left half — main inputs
                claim = gr.Textbox(label="Enter Claim")
                text_button = gr.Button("Classify Claim", interactive=False) # Disable until tokens provided
                text_output = gr.Markdown( label="Model Output", value="Results will appear here...")

            
            with gr.Column(scale=1):  # Right half — user token inputs
                gr.Markdown("## Enter your API keys")
                hf_token = gr.Textbox(label="Hugging Face Token 🔴", type="password")
                tavily_key = gr.Textbox(label="Tavily API Key 🔴", type="password")
                google_key = gr.Textbox(label="Google Fact Check API Key 🔴", type="password")
            
        # Enable button when all fields filled
        def enable_button(hf, tavily, google):
            ready = bool(hf and tavily and google)
            return gr.update(interactive=ready)
        
        hf_token.change(enable_button, inputs=[hf_token, tavily_key, google_key], outputs=text_button)
        tavily_key.change(enable_button, inputs=[hf_token, tavily_key, google_key], outputs=text_button)
        google_key.change(enable_button, inputs=[hf_token, tavily_key, google_key], outputs=text_button)

        # Click handler (include all token inputs)
        text_button.click(classify_text,
                          inputs=[claim, hf_token, tavily_key, google_key],
                          outputs=text_output)
        

    with gr.Tab("Image Detector"):
        img_input = gr.Image(type="pil", label="Upload Image")
        img_button = gr.Button("Classify Image")
        img_output = gr.Markdown(label="Model Output", value="Results will appear here...")
        
        img_button.click(classify_image, inputs=img_input, outputs=img_output)

demo.launch()