Yadav88 commited on
Commit
1095d06
Β·
verified Β·
1 Parent(s): 90bdd18

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +381 -0
app.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import streamlit as st
3
+ from serpapi import GoogleSearch
4
+ from sentence_transformers import SentenceTransformer, util
5
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from urllib.parse import urlparse
9
+ from bs4 import BeautifulSoup
10
+ import requests
11
+ import re
12
+ import numpy as np
13
+ import textwrap
14
+
15
+ # ---------------- Settings ----------------
16
+ st.set_page_config(page_title="Super Smart Fake News Detector β€” Advanced", page_icon="🧠", layout="centered")
17
+ st.title("🧠 Super Smart Fake News Detector β€” Advanced")
18
+ st.write("Dynamic verdict (TRUE / FAKE / INSUFFICIENT DATA) using Semantic Similarity, NLI (Entailment/Contradiction), and Credibility weighting.")
19
+
20
+ # --- FULL POWER CONSTANTS ---
21
+ NUM_RESULTS = 30 # Maximum number of search results to fetch
22
+ TOP_K_FOR_VERDICT = 6 # Maximum number of top results to analyze
23
+ # ----------------------------
24
+
25
+ # ---------------- Caches / Model loaders ----------------
26
+ @st.cache_resource
27
+ def load_embedder():
28
+ # Force CPU to avoid device issues on Cloud
29
+ return SentenceTransformer('all-MiniLM-L6-v2', device='cpu')
30
+
31
+ @st.cache_resource
32
+ def load_nli_model():
33
+ # NLI model (roberta-large-mnli) - CPU mode
34
+ tok = AutoTokenizer.from_pretrained("roberta-large-mnli")
35
+ mdl = AutoModelForSequenceClassification.from_pretrained("roberta-large-mnli")
36
+ mdl.to("cpu")
37
+ return tok, mdl
38
+
39
+ embedder = load_embedder()
40
+ nli_tok, nli_model = load_nli_model()
41
+
42
+ # ---------------- Utilities ----------------
43
+ def domain_from_url(url):
44
+ try:
45
+ return urlparse(url).netloc.replace("www.", "")
46
+ except:
47
+ return url
48
+
49
+ def pretty_pct(x):
50
+ return f"{int(x*100)}%"
51
+
52
+ # ---------------- Rank-claim helpers (Wikipedia list check) ----------------
53
+ ORDINAL_WORDS = {
54
+ "first":1, "second":2, "third":3, "fourth":4, "fifth":5, "sixth":6, "seventh":7, "eighth":8, "ninth":9, "tenth":10,
55
+ "eleventh":11, "twelfth":12, "thirteenth":13, "fourteenth":14, "fifteenth":15, "sixteenth":16, "seventeenth":17,
56
+ "eighteenth":18, "nineteenth":19, "twentieth":20
57
+ }
58
+ ROLE_KEYWORDS = ["prime minister", "prime-minister", "pm", "president", "chief minister", "cm", "governor", "chief justice"]
59
+
60
+ def find_ordinal_and_role(text):
61
+ t = text.lower()
62
+ num = None
63
+ m = re.search(r'\b(\d{1,2})(?:st|nd|rd|th)?\b', t)
64
+ if m:
65
+ num = int(m.group(1))
66
+ else:
67
+ for w, n in ORDINAL_WORDS.items():
68
+ if re.search(r'\b' + re.escape(w) + r'\b', t):
69
+ num = n
70
+ break
71
+ role = None
72
+ for rk in ROLE_KEYWORDS:
73
+ if rk in t:
74
+ role = rk.replace('-', ' ')
75
+ break
76
+ return num, role
77
+
78
+ def extract_person_candidate(text):
79
+ patterns = [
80
+ r"^([\w\s\.\-]{2,80}?)\s+is\s+the\b",
81
+ r"^([\w\s\.\-]{2,80}?)\s+is\s+(\d{1,2})",
82
+ r"is\s+([\w\s\.\-]{2,80}?)\s+the\s+\d{1,2}",
83
+ r"^([\w\s\.\-]{2,80}?)\s+was\s+the\b",
84
+ ]
85
+ for p in patterns:
86
+ mm = re.search(p, text, flags=re.IGNORECASE)
87
+ if mm:
88
+ name = mm.group(1).strip()
89
+ if len(name) > 1 and not re.match(r'^(it|he|she|they|this|that)$', name.lower()):
90
+ return name
91
+ tokens = re.findall(r'[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*', text)
92
+ if tokens:
93
+ return tokens[0]
94
+ return text.split()[0]
95
+
96
+ def normalize_name(s):
97
+ return re.sub(r'[^a-z]', '', s.lower())
98
+
99
+ def find_wikipedia_list_page(role, country, serp_api_key):
100
+ query = f'List of {role} of {country} site:en.wikipedia.org'
101
+ try:
102
+ params = {"engine":"google", "q": query, "api_key": st.secrets["SERPAPI_KEY"], "num": 1}
103
+ search = GoogleSearch(params)
104
+ res = search.get_dict()
105
+ organic = res.get("organic_results") or []
106
+ if organic:
107
+ return organic[0].get("link")
108
+ except Exception:
109
+ pass
110
+ cand = f"https://en.wikipedia.org/wiki/List_of_{role.replace(' ','_')}_of_{country.replace(' ','_')}"
111
+ return cand
112
+
113
+ def parse_wikipedia_list(url):
114
+ try:
115
+ r = requests.get(url, timeout=8, headers={"User-Agent":"Mozilla/5.0"})
116
+ if r.status_code != 200:
117
+ return []
118
+ soup = BeautifulSoup(r.text, 'html.parser')
119
+ names = []
120
+ tables = soup.find_all("table", {"class": ["wikitable", "sortable"]})
121
+ for table in tables:
122
+ for tr in table.find_all("tr"):
123
+ tds = tr.find_all(["td", "th"])
124
+ if not tds:
125
+ continue
126
+ textcells = [td.get_text(separator=" ").strip() for td in tds if td.get_text(strip=True)]
127
+ for cell in textcells[:2]:
128
+ if re.search(r'\b(19|20)\d{2}\b', cell) and len(cell) < 30:
129
+ continue
130
+ if len(cell) > 1 and re.search(r'[A-Za-z]', cell):
131
+ cleaned = re.sub(r'\[.*?\]|\(.*?\)', '', cell).strip()
132
+ cand = re.split(r'\n|,|;|-', cleaned)[0].strip()
133
+ if len(cand) > 1 and not re.search(r'\b(year|term|born)\b', cand, re.I):
134
+ names.append(cand)
135
+ break
136
+ if not names:
137
+ for li in soup.find_all('li'):
138
+ text = li.get_text().strip()
139
+ if len(text) > 3 and re.search(r'\b[A-Z][a-z]+', text):
140
+ if re.search(r'\b(19|20)\d{2}\b', text) or re.search(r'\bPrime Minister\b', text, re.I):
141
+ cleaned = re.sub(r'\[.*?\]|\(.*?\)', '', text).strip()
142
+ names.append(cleaned.split('β€”')[0].split('-')[0].strip())
143
+ normalized = []
144
+ for n in names:
145
+ nn = re.sub(r'\s+', ' ', n).strip()
146
+ if nn and nn not in normalized:
147
+ normalized.append(nn)
148
+ return normalized
149
+ except Exception:
150
+ return []
151
+
152
+ def match_person_in_list(person_candidate, names_list):
153
+ pc = normalize_name(person_candidate)
154
+ for idx, full in enumerate(names_list):
155
+ if not full:
156
+ continue
157
+ fn = normalize_name(full)
158
+ if pc and (pc in fn or fn in pc):
159
+ return idx+1, full
160
+ tokens = person_candidate.lower().split()
161
+ for idx, full in enumerate(names_list):
162
+ fn = full.lower()
163
+ if all(any(tok in part for part in fn.split()) for tok in tokens if len(tok)>2):
164
+ return idx+1, full
165
+ return None, None
166
+
167
+ def check_rank_claim_wikipedia(person, ordinal, role, country, serp_api_key):
168
+ wiki_url = find_wikipedia_list_page(role, country, serp_api_key)
169
+ names = parse_wikipedia_list(wiki_url)
170
+ if not names:
171
+ return {"decisive": False, "reason": "Could not retrieve list page or parse it.", "wiki_url": wiki_url}
172
+ rank, matched_name = match_person_in_list(person, names)
173
+ if rank is None:
174
+ return {"decisive": False, "reason": "Person not found in list parsed from " + wiki_url, "wiki_url": wiki_url, "names_sample": names[:6]}
175
+ else:
176
+ if rank == ordinal:
177
+ return {"decisive": True, "result": True, "rank": rank, "matched_name": matched_name, "wiki_url": wiki_url}
178
+ else:
179
+ return {"decisive": True, "result": False, "rank": rank, "matched_name": matched_name, "wiki_url": wiki_url}
180
+
181
+ # ---------------- NLI & sentence helpers ----------------
182
+ def nli_entailment_prob(premise, hypothesis):
183
+ inputs = nli_tok.encode_plus(premise, hypothesis, return_tensors="pt", truncation=True, max_length=512)
184
+ inputs = {k: v.to('cpu') for k, v in inputs.items()}
185
+ with torch.no_grad():
186
+ logits = nli_model(**inputs).logits
187
+ probs = F.softmax(logits, dim=1)[0]
188
+ # NLI labels for roberta-large-mnli are: 0=entailment, 1=neutral, 2=contradiction
189
+ return probs[0].item(), probs[1].item(), probs[2].item() # Entailment, Neutral, Contradiction
190
+
191
+ def best_sentence_for_claim(snippet, claim):
192
+ import re
193
+ sents = re.split(r'(?<=[.!?])\s+', snippet) if snippet else []
194
+ if not sents:
195
+ return snippet or "", 0.0
196
+ sent_embs = embedder.encode(sents, convert_to_tensor=True)
197
+ claim_emb = embedder.encode(claim, convert_to_tensor=True)
198
+ sims = util.cos_sim(claim_emb, sent_embs)[0].cpu().numpy()
199
+ best_idx = int(sims.argmax())
200
+ return sents[best_idx], float(sims[best_idx])
201
+
202
+ def domain_boost(domain):
203
+ trusted = ["bbc", "reuters", "theguardian", "nytimes", "indiatimes", "ndtv", "timesofindia", "cnn", "espn", "espncricinfo", "aljazeera"]
204
+ return 0.2 if any(t in domain for t in trusted) else 0.0
205
+
206
+ def analyze_top_articles(normalized, claim, top_k):
207
+ sims, entails, neutral, contradicts, creds = [], [], [], [], []
208
+ for r in normalized[:top_k]:
209
+ text = (r.get("title","") + ". " + (r.get("snippet") or ""))
210
+ best_sent, best_sim = best_sentence_for_claim(r.get("snippet",""), claim)
211
+ # fallback semantic sim using whole text if best_sim==0
212
+ sem_sim = best_sim if best_sim>0 else float(util.cos_sim(
213
+ embedder.encode(claim, convert_to_tensor=True),
214
+ embedder.encode(text, convert_to_tensor=True)
215
+ )[0].item())
216
+
217
+ try:
218
+ entail_p, neutral_p, contra_p = nli_entailment_prob(best_sent or text, claim)
219
+ except Exception:
220
+ entail_p, neutral_p, contra_p = 0.0, 0.0, 0.0
221
+
222
+ # --- NLI Smart Filter (Fixes high contradiction on high sim matches) ---
223
+ if sem_sim > 0.80 and contra_p > 0.80 and entail_p < 0.10:
224
+ # Assume this is the correct news headline reporting the claim, not contradicting it.
225
+ entail_p = 0.80
226
+ contra_p = 0.05
227
+ # ----------------------------------------------------------------------
228
+
229
+ domain = urlparse(r.get("link","")).netloc
230
+ cred = domain_boost(domain)
231
+
232
+ sims.append(sem_sim)
233
+ entails.append(entail_p)
234
+ neutral.append(neutral_p)
235
+ contradicts.append(contra_p)
236
+ creds.append(cred)
237
+
238
+ r["entail_p"] = entail_p
239
+ r["neutral_p"] = neutral_p
240
+ r["contra_p"] = contra_p
241
+ r["sem_sim"] = sem_sim
242
+ r["cred"] = cred
243
+ r["best_sent"] = best_sent
244
+
245
+ avg_sim = float(np.mean(sims)) if sims else 0.0
246
+ avg_ent = float(np.mean(entails)) if entails else 0.0
247
+ avg_neu = float(np.mean(neutral)) if neutral else 0.0
248
+ avg_con = float(np.mean(contradicts)) if contradicts else 0.0
249
+ avg_cred = float(np.mean(creds)) if creds else 0.0
250
+
251
+ # Calculate net support as (Entailment - Contradiction)
252
+ net_support = avg_ent - avg_con
253
+
254
+ # DYNAMIC SCORING LOGIC
255
+ # SCORE 1: Support Score (Prioritizes credible logical support)
256
+ # This is the primary decision factor: Net Support * (1 + Credibility)
257
+ support_score = net_support * (1 + avg_cred)
258
+
259
+ # SCORE 2: Final Score (Used for general ranking/transparency)
260
+ final_score = 0.50 * net_support + 0.30 * avg_sim + 0.20 * avg_cred
261
+
262
+ metrics = {
263
+ "avg_ent": avg_ent,
264
+ "avg_neu": avg_neu,
265
+ "avg_con": avg_con,
266
+ "avg_sim": avg_sim,
267
+ "avg_cred": avg_cred,
268
+ "net_support": net_support,
269
+ "support_score": support_score
270
+ }
271
+ return final_score, metrics, normalized[:top_k]
272
+
273
+ # ---------------- Main UI inputs ----------------
274
+ claim = st.text_area("Enter claim or news sentence:", height=140, placeholder="e.g. India defeats Pakistan in Asia Cup 2025")
275
+
276
+ st.info(f"Using **{NUM_RESULTS}** recent news results (Last 24hrs) and analyzing top **{TOP_K_FOR_VERDICT}** matches (Full Power Mode).")
277
+
278
+ if st.button("Verify Claim"):
279
+ if not claim.strip():
280
+ st.warning("Please enter a claim.")
281
+ else:
282
+ with st.spinner("Analysing... (this may take a few seconds)"):
283
+
284
+ # 1) Rank-claim check (Wikipedia) if applicable
285
+ ordinal, role = find_ordinal_and_role(claim)
286
+ person_candidate = None
287
+ country = "India" if "india" in claim.lower() else ""
288
+ if ordinal and role:
289
+ person_candidate = extract_person_candidate(claim)
290
+ m_country = re.search(r'\bof\s+([A-Za-z\s]+)', claim, flags=re.IGNORECASE)
291
+ if m_country:
292
+ country = m_country.group(1).strip()
293
+ rank_check = check_rank_claim_wikipedia(person_candidate, ordinal, role, country or "India", st.secrets["SERPAPI_KEY"])
294
+ if rank_check.get("decisive"):
295
+ if rank_check.get("result"):
296
+ st.markdown("<h2 style='color:green;text-align:center'>βœ… TRUE</h2>", unsafe_allow_html=True)
297
+ st.write(f"Reason: Authoritative list ({rank_check.get('wiki_url')}) shows **{rank_check.get('matched_name')}** as the {ordinal}th {role} of {country or 'the country'}.")
298
+ else:
299
+ st.markdown("<h2 style='color:red;text-align:center'>🚨 FAKE</h2>", unsafe_allow_html=True)
300
+ st.write(f"Reason: Authoritative list ({rank_check.get('wiki_url')}) shows **{rank_check.get('matched_name')}** as the {rank_check.get('rank')}th {role}, not the {ordinal}th.")
301
+ st.write("Source (for verification):", rank_check.get("wiki_url"))
302
+ st.stop() # done
303
+
304
+ # 2) SerpAPI fetch (Filtering results to last 24hrs using tbs=qdr:d1)
305
+ try:
306
+ # Using tbs=qdr:d1 to filter results to the last 24 hours for better relevance
307
+ params = {"engine":"google", "q": claim, "tbm":"nws", "tbs":"qdr:d1", "num": NUM_RESULTS, "api_key": st.secrets["SERPAPI_KEY"]}
308
+ search = GoogleSearch(params)
309
+ data = search.get_dict()
310
+ results = data.get("news_results") or data.get("organic_results") or []
311
+ except Exception as e:
312
+ st.error("Search failed: " + str(e))
313
+ results = []
314
+
315
+ if not results:
316
+ st.markdown("<h2 style='color:red;text-align:center'>🚨 FAKE</h2>", unsafe_allow_html=True)
317
+ st.write("Reason: No relevant **recent** news results returned by the live search API. The claim is unconfirmed or outdated.")
318
+ else:
319
+ normalized = []
320
+ for r in results:
321
+ title = r.get("title") or r.get("title_raw") or r.get("title_original") or ""
322
+ snippet = r.get("snippet") or r.get("snippet_highlighted") or r.get("excerpt") or ""
323
+ link = r.get("link") or r.get("source", {}).get("url") or r.get("source_link") or ""
324
+ normalized.append({"title": title, "snippet": snippet, "link": link})
325
+
326
+ # compute decision via new intelligence module
327
+ final_score, metrics, analyzed = analyze_top_articles(normalized, claim, top_k=TOP_K_FOR_VERDICT)
328
+
329
+ # DYNAMIC VERDICT LOGIC: (TRUE / FAKE / INSUFFICIENT DATA)
330
+
331
+ # Condition for TRUE: High credibility-weighted support AND good relevance.
332
+ if metrics["support_score"] >= 0.15 and metrics["avg_sim"] >= 0.50:
333
+ st.markdown("<h2 style='color:green;text-align:center'>βœ… TRUE</h2>", unsafe_allow_html=True)
334
+ st.write("Reason: **Strong logical support from credible sources** found, confirming the claim's relevance.")
335
+ verdict_msg = "TRUE"
336
+
337
+ # Condition for INSUFFICIENT DATA: Not enough support (low support_score) and low relevance, but high neutrality (no strong contradiction found).
338
+ elif metrics["avg_sim"] < 0.50 and metrics["avg_neu"] > 0.60:
339
+ st.markdown("<h2 style='color:orange;text-align:center'>⚠️ INSUFFICIENT DATA</h2>", unsafe_allow_html=True)
340
+ st.write("Reason: Low semantic relevance and high neutral logical probability across sources. The claim is either too vague, futuristic, or lacks sufficient recent confirmation.")
341
+ verdict_msg = "INSUFFICIENT DATA"
342
+
343
+ # Default to FAKE: Insufficient support or strong contradiction present.
344
+ else:
345
+ st.markdown("<h2 style='color:red;text-align:center'>🚨 FAKE</h2>", unsafe_allow_html=True)
346
+ st.write("Reason: Insufficient combined credibility and logical support, or strong refutation present. The claim is likely refuted, outdated, or lacks reliable confirmation.")
347
+ verdict_msg = "FAKE"
348
+
349
+
350
+ st.write(f"Details β€” Support Score (Credibility Weighted): {metrics['support_score']:.2f}, avg semantic sim: {metrics['avg_sim']:.2f}, net support (E-C): {metrics['net_support']:.2f}")
351
+
352
+
353
+ # show short synthesized reason
354
+ if verdict_msg == "TRUE":
355
+ ex = []
356
+ for r in analyzed[:3]:
357
+ if r.get("sem_sim", 0.0) > 0.4 and r.get("entail_p", 0.0) > r.get("contra_p", 0.0):
358
+ ex.append(textwrap.shorten(r.get("best_sent") or r.get("snippet",""), width=160, placeholder="..."))
359
+ if ex:
360
+ st.info("Example supporting excerpts: " + " | ".join(ex))
361
+ elif verdict_msg == "FAKE":
362
+ best = analyzed[0] if analyzed else None
363
+ if best and best.get("best_sent"):
364
+ st.info("Closest (but weak) excerpt: " + textwrap.shorten(best.get("best_sent") or best.get("snippet",""), width=220, placeholder="..."))
365
+
366
+ # transparency
367
+ with st.expander("Show analyzed top sources and scores"):
368
+ for idx, r in enumerate(analyzed):
369
+ st.markdown(f"**{idx+1}. {r.get('title') or r.get('link','(no title)')}**")
370
+ st.write(f"- Domain: {domain_from_url(r.get('link',''))}")
371
+ st.write(f"- Semantic similarity (sentence-level): {pretty_pct(r.get('sem_sim',0.0))}")
372
+ st.write(f"- **Net Support (Entail-Contra)**: {r.get('entail_p',0.0) - r.get('contra_p',0.0):.2f}")
373
+ st.write(f" (E: {pretty_pct(r.get('entail_p',0.0))} | N: {pretty_pct(r.get('neutral_p',0.0))} | C: {pretty_pct(r.get('contra_p',0.0))})")
374
+ st.write(f"- Credibility boost: {r.get('cred',0.0):.2f}")
375
+ st.write(f"- Link: {r.get('link')}")
376
+ st.markdown("---")
377
+
378
+
379
+ # Footer
380
+ st.markdown("---")
381
+ st.caption("Project: NLP-driven Fact-Checking System. Use responsibly.")