chirag1121 commited on
Commit
0fd505b
Β·
verified Β·
1 Parent(s): fb8a4e8

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +657 -0
app.py ADDED
@@ -0,0 +1,657 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import torch
4
+ import numpy as np
5
+ import plotly.graph_objects as go
6
+ import plotly.express as px
7
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
8
+ from datetime import datetime
9
+ import pandas as pd
10
+ import re
11
+ from urllib.parse import urlparse
12
+
13
+ # ─────────────────────────────────────────────
14
+ # πŸ”‘ API KEY β€” PASTE YOUR NewsAPI KEY HERE
15
+ NEWS_API_KEY = "YOUR_NEWSAPI_KEY_HERE"
16
+ # Get your free key at https://newsapi.org/register
17
+ # ─────────────────────────────────────────────
18
+
19
+ # ─────────────────────────────────────────────
20
+ # Model: Pre-trained fine-tuned BERT for fake news
21
+ # No training needed β€” loaded directly from HuggingFace Hub
22
+ MODEL_NAME = "hamzab/roberta-fake-news-classification"
23
+ # ─────────────────────────────────────────────
24
+
25
+ # Source credibility database
26
+ SOURCE_CREDIBILITY = {
27
+ # High credibility (score: 0.9–1.0)
28
+ "bbc.com": 0.97, "bbc.co.uk": 0.97,
29
+ "reuters.com": 0.96, "apnews.com": 0.95,
30
+ "theguardian.com": 0.93, "nytimes.com": 0.92,
31
+ "washingtonpost.com": 0.91, "npr.org": 0.92,
32
+ "bloomberg.com": 0.90, "economist.com": 0.92,
33
+ "ft.com": 0.91, "nature.com": 0.97,
34
+ "science.org": 0.97, "who.int": 0.98,
35
+ "cdc.gov": 0.97, "gov.uk": 0.94,
36
+ "thehindu.com": 0.88, "ndtv.com": 0.82,
37
+ "hindustantimes.com": 0.80, "timesofindia.com": 0.79,
38
+ # Medium credibility (0.5–0.8)
39
+ "cnn.com": 0.78, "foxnews.com": 0.65,
40
+ "huffpost.com": 0.70, "buzzfeed.com": 0.62,
41
+ "vice.com": 0.68, "vox.com": 0.74,
42
+ "medium.com": 0.52, "substack.com": 0.50,
43
+ # Low credibility (< 0.5) β€” examples of known misinformation sites
44
+ "infowars.com": 0.05, "naturalnews.com": 0.08,
45
+ "beforeitsnews.com": 0.06, "worldnewsdailyreport.com": 0.04,
46
+ "empirenews.net": 0.04, "theonion.com": 0.10,
47
+ }
48
+
49
+ CREDIBILITY_LABELS = {
50
+ (0.85, 1.0): ("🟒 Highly Credible", "#22c55e"),
51
+ (0.65, 0.85): ("🟑 Moderately Credible", "#f59e0b"),
52
+ (0.40, 0.65): ("🟠 Low Credibility", "#f97316"),
53
+ (0.0, 0.40): ("πŸ”΄ Very Low / Known Misinformation", "#ef4444"),
54
+ }
55
+
56
+ FAKE_INDICATORS = [
57
+ (r'\b(SHOCKING|BOMBSHELL|BREAKING|EXCLUSIVE)\b', "ALL-CAPS sensational trigger words"),
58
+ (r'(!{2,}|\?{2,})', "Excessive punctuation (!! or ??)"),
59
+ (r'\b(they don\'t want you to know|mainstream media won\'t tell)\b', "Anti-establishment conspiracy framing"),
60
+ (r'\b(miracle|cure|secret|censored|banned)\b', "Clickbait / pseudoscience language"),
61
+ (r'\b(100%|proven fact|scientists hate)\b', "Overconfident absolute claims"),
62
+ (r'(share before deleted|share before banned)', "Urgency/fear-of-censorship manipulation"),
63
+ (r'\b(deep state|new world order|illuminati|cabal)\b', "Conspiracy theory terminology"),
64
+ (r'\baccording to sources\b(?!.*\bnamed\b)', "Vague anonymous sourcing"),
65
+ ]
66
+
67
+
68
+ @st.cache_resource(show_spinner=False)
69
+ def load_model():
70
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
71
+ model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)
72
+ model.eval()
73
+ return tokenizer, model
74
+
75
+
76
+ def classify_text(text, tokenizer, model):
77
+ inputs = tokenizer(text, return_tensors="pt", truncation=True,
78
+ max_length=512, padding=True)
79
+ with torch.no_grad():
80
+ outputs = model(**inputs)
81
+ probs = torch.softmax(outputs.logits, dim=1).squeeze().numpy()
82
+
83
+ # Model labels: 0 = FAKE, 1 = REAL (adjust if needed for your model)
84
+ labels = model.config.id2label
85
+ fake_idx = next((i for i, l in labels.items() if "fake" in l.lower() or "0" == str(i)), 0)
86
+ real_idx = 1 - fake_idx
87
+
88
+ fake_prob = float(probs[fake_idx])
89
+ real_prob = float(probs[real_idx])
90
+ prediction = "FAKE" if fake_prob > real_prob else "REAL"
91
+ confidence = max(fake_prob, real_prob)
92
+ return prediction, confidence, fake_prob, real_prob
93
+
94
+
95
+ def get_source_credibility(url_or_domain):
96
+ if not url_or_domain:
97
+ return None, 0.5, "Unknown Source"
98
+ try:
99
+ domain = urlparse(url_or_domain).netloc.lower().replace("www.", "")
100
+ except Exception:
101
+ domain = url_or_domain.lower().replace("www.", "")
102
+
103
+ if domain in SOURCE_CREDIBILITY:
104
+ score = SOURCE_CREDIBILITY[domain]
105
+ else:
106
+ # Heuristics for unknown sources
107
+ score = 0.45 # default unknown
108
+ if domain.endswith(".gov") or domain.endswith(".edu"):
109
+ score = 0.90
110
+ elif domain.endswith(".org"):
111
+ score = 0.65
112
+
113
+ label, color = "Unknown", "#94a3b8"
114
+ for (low, high), (lbl, clr) in CREDIBILITY_LABELS.items():
115
+ if low <= score <= high:
116
+ label, color = lbl, clr
117
+ break
118
+ return domain, score, label, color
119
+
120
+
121
+ def detect_fake_indicators(text):
122
+ found = []
123
+ for pattern, description in FAKE_INDICATORS:
124
+ if re.search(pattern, text, re.IGNORECASE):
125
+ found.append(description)
126
+ return found
127
+
128
+
129
+ def fetch_news(query, api_key, max_articles=6):
130
+ if not api_key or api_key == "YOUR_NEWSAPI_KEY_HERE":
131
+ return None, "⚠️ No API key provided. Add your NewsAPI key in app.py."
132
+ url = (
133
+ f"https://newsapi.org/v2/everything?"
134
+ f"q={requests.utils.quote(query)}&language=en&sortBy=publishedAt"
135
+ f"&pageSize={max_articles}&apiKey={api_key}"
136
+ )
137
+ try:
138
+ resp = requests.get(url, timeout=10)
139
+ data = resp.json()
140
+ if data.get("status") != "ok":
141
+ return None, data.get("message", "API error")
142
+ return data.get("articles", []), None
143
+ except Exception as e:
144
+ return None, str(e)
145
+
146
+
147
+ def make_confidence_gauge(fake_prob, real_prob):
148
+ fig = go.Figure(go.Indicator(
149
+ mode="gauge+number+delta",
150
+ value=round(fake_prob * 100, 1),
151
+ domain={"x": [0, 1], "y": [0, 1]},
152
+ title={"text": "Fake Probability %", "font": {"size": 18, "color": "#e2e8f0"}},
153
+ number={"font": {"size": 36, "color": "#f8fafc"}, "suffix": "%"},
154
+ gauge={
155
+ "axis": {"range": [0, 100], "tickcolor": "#64748b",
156
+ "tickfont": {"color": "#94a3b8"}},
157
+ "bar": {"color": "#6366f1"},
158
+ "steps": [
159
+ {"range": [0, 30], "color": "#14532d"},
160
+ {"range": [30, 55], "color": "#713f12"},
161
+ {"range": [55, 100], "color": "#7f1d1d"},
162
+ ],
163
+ "threshold": {
164
+ "line": {"color": "#fbbf24", "width": 4},
165
+ "thickness": 0.85,
166
+ "value": 50,
167
+ },
168
+ },
169
+ ))
170
+ fig.update_layout(
171
+ paper_bgcolor="rgba(0,0,0,0)",
172
+ plot_bgcolor="rgba(0,0,0,0)",
173
+ font={"color": "#e2e8f0"},
174
+ height=280,
175
+ margin=dict(t=50, b=10, l=30, r=30),
176
+ )
177
+ return fig
178
+
179
+
180
+ def make_prob_bar(fake_prob, real_prob):
181
+ fig = go.Figure()
182
+ fig.add_trace(go.Bar(
183
+ x=["FAKE", "REAL"],
184
+ y=[fake_prob * 100, real_prob * 100],
185
+ marker_color=["#ef4444", "#22c55e"],
186
+ text=[f"{fake_prob*100:.1f}%", f"{real_prob*100:.1f}%"],
187
+ textposition="outside",
188
+ textfont=dict(color="#f8fafc", size=14),
189
+ width=0.45,
190
+ ))
191
+ fig.update_layout(
192
+ paper_bgcolor="rgba(0,0,0,0)",
193
+ plot_bgcolor="rgba(0,0,0,0)",
194
+ font=dict(color="#e2e8f0"),
195
+ yaxis=dict(range=[0, 115], gridcolor="#1e293b", ticksuffix="%",
196
+ tickfont=dict(color="#64748b")),
197
+ xaxis=dict(tickfont=dict(color="#e2e8f0", size=14)),
198
+ height=260,
199
+ margin=dict(t=10, b=10, l=10, r=10),
200
+ showlegend=False,
201
+ )
202
+ return fig
203
+
204
+
205
+ def credibility_bar_chart(domain, score):
206
+ fig = go.Figure(go.Bar(
207
+ x=[score * 100],
208
+ y=[domain or "Unknown"],
209
+ orientation="h",
210
+ marker=dict(
211
+ color=score * 100,
212
+ colorscale=[[0, "#ef4444"], [0.5, "#f59e0b"], [1, "#22c55e"]],
213
+ cmin=0, cmax=100,
214
+ ),
215
+ text=[f"{score*100:.0f}/100"],
216
+ textposition="outside",
217
+ textfont=dict(color="#f8fafc"),
218
+ ))
219
+ fig.update_layout(
220
+ paper_bgcolor="rgba(0,0,0,0)",
221
+ plot_bgcolor="rgba(0,0,0,0)",
222
+ xaxis=dict(range=[0, 115], gridcolor="#1e293b", ticksuffix="",
223
+ tickfont=dict(color="#64748b")),
224
+ yaxis=dict(tickfont=dict(color="#e2e8f0", size=13)),
225
+ height=120,
226
+ margin=dict(t=5, b=5, l=10, r=60),
227
+ )
228
+ return fig
229
+
230
+
231
+ # ══════════════════════════════════════════════
232
+ # STREAMLIT UI
233
+ # ══════════════════════════════════════════════
234
+ st.set_page_config(
235
+ page_title="FakeScope β€” AI News Verifier",
236
+ page_icon="πŸ”",
237
+ layout="wide",
238
+ initial_sidebar_state="expanded",
239
+ )
240
+
241
+ st.markdown("""
242
+ <style>
243
+ @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
244
+
245
+ html, body, [class*="css"] {
246
+ font-family: 'Syne', sans-serif;
247
+ background-color: #050a14;
248
+ color: #e2e8f0;
249
+ }
250
+ .stApp { background: #050a14; }
251
+
252
+ /* Hero banner */
253
+ .hero {
254
+ background: linear-gradient(135deg, #0f172a 0%, #1a0a2e 50%, #0f172a 100%);
255
+ border: 1px solid #1e293b;
256
+ border-radius: 20px;
257
+ padding: 2.5rem 3rem;
258
+ margin-bottom: 2rem;
259
+ position: relative;
260
+ overflow: hidden;
261
+ }
262
+ .hero::before {
263
+ content: '';
264
+ position: absolute;
265
+ top: -60px; right: -60px;
266
+ width: 300px; height: 300px;
267
+ background: radial-gradient(circle, rgba(99,102,241,0.15) 0%, transparent 70%);
268
+ border-radius: 50%;
269
+ }
270
+ .hero h1 {
271
+ font-size: 3rem; font-weight: 800;
272
+ background: linear-gradient(90deg, #818cf8, #c084fc, #f472b6);
273
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
274
+ margin: 0; letter-spacing: -1px;
275
+ }
276
+ .hero p { color: #94a3b8; font-size: 1.05rem; margin-top: 0.5rem; margin-bottom: 0; }
277
+
278
+ /* Cards */
279
+ .card {
280
+ background: #0f172a;
281
+ border: 1px solid #1e293b;
282
+ border-radius: 16px;
283
+ padding: 1.5rem;
284
+ margin-bottom: 1rem;
285
+ }
286
+ .verdict-fake {
287
+ border: 2px solid #ef4444;
288
+ background: linear-gradient(135deg, #1a0000, #0f172a);
289
+ border-radius: 16px;
290
+ padding: 1.5rem;
291
+ text-align: center;
292
+ }
293
+ .verdict-real {
294
+ border: 2px solid #22c55e;
295
+ background: linear-gradient(135deg, #001a00, #0f172a);
296
+ border-radius: 16px;
297
+ padding: 1.5rem;
298
+ text-align: center;
299
+ }
300
+ .verdict-label {
301
+ font-size: 2.5rem; font-weight: 800; letter-spacing: 4px;
302
+ }
303
+ .fake-label { color: #ef4444; }
304
+ .real-label { color: #22c55e; }
305
+
306
+ /* Indicator pills */
307
+ .indicator-pill {
308
+ display: inline-block;
309
+ background: #1e1030;
310
+ border: 1px solid #7c3aed;
311
+ color: #c084fc;
312
+ border-radius: 99px;
313
+ padding: 0.3rem 0.9rem;
314
+ font-size: 0.82rem;
315
+ margin: 0.25rem;
316
+ font-family: 'JetBrains Mono', monospace;
317
+ }
318
+
319
+ /* News cards */
320
+ .news-card {
321
+ background: #0f172a;
322
+ border: 1px solid #1e293b;
323
+ border-radius: 12px;
324
+ padding: 1.2rem;
325
+ margin-bottom: 0.8rem;
326
+ transition: border-color 0.2s;
327
+ }
328
+ .news-card:hover { border-color: #6366f1; }
329
+ .news-card h4 { color: #e2e8f0; font-size: 0.95rem; margin: 0 0 0.4rem 0; }
330
+ .news-card p { color: #64748b; font-size: 0.82rem; margin: 0; }
331
+
332
+ /* Sidebar */
333
+ section[data-testid="stSidebar"] {
334
+ background: #080d1a;
335
+ border-right: 1px solid #1e293b;
336
+ }
337
+
338
+ /* Inputs */
339
+ .stTextArea textarea, .stTextInput input {
340
+ background: #0f172a !important;
341
+ border: 1px solid #334155 !important;
342
+ color: #e2e8f0 !important;
343
+ border-radius: 10px !important;
344
+ font-family: 'JetBrains Mono', monospace !important;
345
+ }
346
+ .stButton > button {
347
+ background: linear-gradient(135deg, #4f46e5, #7c3aed) !important;
348
+ color: white !important;
349
+ border: none !important;
350
+ border-radius: 10px !important;
351
+ font-family: 'Syne', sans-serif !important;
352
+ font-weight: 700 !important;
353
+ font-size: 1rem !important;
354
+ padding: 0.6rem 2rem !important;
355
+ transition: opacity 0.2s !important;
356
+ width: 100%;
357
+ }
358
+ .stButton > button:hover { opacity: 0.85 !important; }
359
+
360
+ /* Section headers */
361
+ .section-title {
362
+ font-size: 0.75rem; font-weight: 700; letter-spacing: 3px;
363
+ color: #6366f1; text-transform: uppercase; margin-bottom: 0.75rem;
364
+ }
365
+
366
+ /* Metric boxes */
367
+ .metric-box {
368
+ background: #0f172a;
369
+ border: 1px solid #1e293b;
370
+ border-radius: 12px;
371
+ padding: 1rem 1.2rem;
372
+ text-align: center;
373
+ }
374
+ .metric-box .val { font-size: 1.8rem; font-weight: 800; color: #818cf8; }
375
+ .metric-box .lbl { font-size: 0.75rem; color: #64748b; letter-spacing: 1px; margin-top: 2px; }
376
+
377
+ div[data-testid="stMetricValue"] { color: #818cf8 !important; font-family: 'Syne', sans-serif !important; }
378
+ </style>
379
+ """, unsafe_allow_html=True)
380
+
381
+ # ── Sidebar ─────────────────────────────────
382
+ with st.sidebar:
383
+ st.markdown("## πŸ” FakeScope")
384
+ st.markdown("---")
385
+ mode = st.radio("**Mode**", ["πŸ“ Paste Article / Text", "🌐 Fetch Live News"])
386
+ st.markdown("---")
387
+ st.markdown("**About the Model**")
388
+ st.caption(f"`{MODEL_NAME}`")
389
+ st.caption("Fine-tuned RoBERTa β€” no local training required.")
390
+ st.markdown("---")
391
+ st.markdown("**Credibility DB**")
392
+ st.caption(f"{len(SOURCE_CREDIBILITY)} known sources indexed.")
393
+ st.markdown("---")
394
+ st.caption("Built with πŸ€— Transformers + Streamlit")
395
+
396
+ # ── Hero ─────────────────────────────────────
397
+ st.markdown("""
398
+ <div class="hero">
399
+ <h1>πŸ”Ž FakeScope</h1>
400
+ <p>AI-powered fake news detector Β· BERT Β· Source Credibility Β· Real-time News Β· Explainability</p>
401
+ </div>
402
+ """, unsafe_allow_html=True)
403
+
404
+ # ── Load model ───────────────────────────────
405
+ with st.spinner("⚑ Loading BERT model from HuggingFace (first run only)…"):
406
+ try:
407
+ tokenizer, model = load_model()
408
+ st.success("βœ… Model loaded successfully!", icon="πŸ€–")
409
+ except Exception as e:
410
+ st.error(f"Model load failed: {e}")
411
+ st.stop()
412
+
413
+ # ════════════════════════════════════════════
414
+ # MODE 1 β€” Paste Text
415
+ # ════════════════════════════════════════════
416
+ if mode == "πŸ“ Paste Article / Text":
417
+ st.markdown('<div class="section-title">Paste news article or headline</div>', unsafe_allow_html=True)
418
+
419
+ col_in, col_meta = st.columns([3, 1])
420
+ with col_in:
421
+ news_text = st.text_area("", height=180,
422
+ placeholder="Paste a news headline, paragraph, or full article here…",
423
+ label_visibility="collapsed")
424
+ with col_meta:
425
+ source_url = st.text_input("Source URL (optional)",
426
+ placeholder="https://bbc.com/…")
427
+ analyze_btn = st.button("πŸ” Analyze", use_container_width=True)
428
+
429
+ if analyze_btn:
430
+ if not news_text.strip():
431
+ st.warning("Please paste some text to analyze.")
432
+ else:
433
+ with st.spinner("Running BERT inference…"):
434
+ prediction, confidence, fake_prob, real_prob = classify_text(
435
+ news_text, tokenizer, model)
436
+ indicators = detect_fake_indicators(news_text)
437
+ domain, cred_score, cred_label, cred_color = get_source_credibility(source_url)
438
+
439
+ # ── Verdict ──────────────────────────────
440
+ st.markdown("---")
441
+ vcol1, vcol2, vcol3 = st.columns([1, 2, 1])
442
+ with vcol2:
443
+ if prediction == "FAKE":
444
+ st.markdown(f"""
445
+ <div class="verdict-fake">
446
+ <div class="verdict-label fake-label">⚠ FAKE NEWS</div>
447
+ <div style="color:#94a3b8;margin-top:0.4rem;font-size:0.95rem;">
448
+ Confidence: <b style="color:#f8fafc">{confidence*100:.1f}%</b>
449
+ </div>
450
+ </div>""", unsafe_allow_html=True)
451
+ else:
452
+ st.markdown(f"""
453
+ <div class="verdict-real">
454
+ <div class="verdict-label real-label">βœ… LIKELY REAL</div>
455
+ <div style="color:#94a3b8;margin-top:0.4rem;font-size:0.95rem;">
456
+ Confidence: <b style="color:#f8fafc">{confidence*100:.1f}%</b>
457
+ </div>
458
+ </div>""", unsafe_allow_html=True)
459
+
460
+ st.markdown("<br>", unsafe_allow_html=True)
461
+
462
+ # ── Charts ───────────────────────────────
463
+ ch1, ch2 = st.columns(2)
464
+ with ch1:
465
+ st.markdown('<div class="section-title">Confidence Gauge</div>', unsafe_allow_html=True)
466
+ st.plotly_chart(make_confidence_gauge(fake_prob, real_prob),
467
+ use_container_width=True, config={"displayModeBar": False})
468
+ with ch2:
469
+ st.markdown('<div class="section-title">Probability Distribution</div>', unsafe_allow_html=True)
470
+ st.plotly_chart(make_prob_bar(fake_prob, real_prob),
471
+ use_container_width=True, config={"displayModeBar": False})
472
+
473
+ # ── Source Credibility ───────────────────
474
+ st.markdown('<div class="section-title">Source Credibility Score</div>', unsafe_allow_html=True)
475
+ st.markdown(f"""
476
+ <div class="card">
477
+ <span style="font-size:1.1rem">{cred_label}</span>
478
+ <span style="color:#64748b;font-family:monospace;font-size:0.85rem;margin-left:1rem">{domain or 'Unknown domain'}</span>
479
+ </div>""", unsafe_allow_html=True)
480
+ st.plotly_chart(credibility_bar_chart(domain or "Unknown", cred_score),
481
+ use_container_width=True, config={"displayModeBar": False})
482
+
483
+ # ── Why it might be fake ─────────────────
484
+ st.markdown('<div class="section-title">🧠 Explanation β€” Why it may be Fake</div>',
485
+ unsafe_allow_html=True)
486
+ with st.container():
487
+ if indicators:
488
+ st.markdown("**Linguistic red flags detected:**")
489
+ pills_html = "".join(
490
+ f'<span class="indicator-pill">⚠ {i}</span>' for i in indicators)
491
+ st.markdown(pills_html, unsafe_allow_html=True)
492
+ else:
493
+ st.success("No obvious linguistic red flags detected in the text.")
494
+
495
+ if prediction == "FAKE":
496
+ reasons = []
497
+ if fake_prob > 0.85:
498
+ reasons.append("Very high BERT fake-probability score (>85%)")
499
+ if cred_score < 0.5:
500
+ reasons.append(f"Source '{domain}' has very low credibility ({cred_score*100:.0f}/100)")
501
+ if indicators:
502
+ reasons.append(f"{len(indicators)} sensational/clickbait linguistic patterns found")
503
+ if reasons:
504
+ st.markdown("**Key reasons for FAKE classification:**")
505
+ for r in reasons:
506
+ st.markdown(f"&nbsp;&nbsp;πŸ”Έ {r}")
507
+
508
+ # ── Stats ────────────────────────────────
509
+ st.markdown('<div class="section-title">Analytics Summary</div>', unsafe_allow_html=True)
510
+ m1, m2, m3, m4 = st.columns(4)
511
+ with m1:
512
+ st.markdown(f'<div class="metric-box"><div class="val">{fake_prob*100:.0f}%</div><div class="lbl">FAKE PROB</div></div>',
513
+ unsafe_allow_html=True)
514
+ with m2:
515
+ st.markdown(f'<div class="metric-box"><div class="val">{real_prob*100:.0f}%</div><div class="lbl">REAL PROB</div></div>',
516
+ unsafe_allow_html=True)
517
+ with m3:
518
+ st.markdown(f'<div class="metric-box"><div class="val">{cred_score*100:.0f}</div><div class="lbl">SOURCE SCORE</div></div>',
519
+ unsafe_allow_html=True)
520
+ with m4:
521
+ st.markdown(f'<div class="metric-box"><div class="val">{len(indicators)}</div><div class="lbl">RED FLAGS</div></div>',
522
+ unsafe_allow_html=True)
523
+
524
+ # ════════════════════════════════════════════
525
+ # MODE 2 β€” Live News Feed
526
+ # ════════════════════════════════════════════
527
+ else:
528
+ st.markdown('<div class="section-title">Fetch & analyze live news articles</div>',
529
+ unsafe_allow_html=True)
530
+
531
+ qcol, bcol = st.columns([4, 1])
532
+ with qcol:
533
+ query = st.text_input("", placeholder="Search topic e.g. 'climate change', 'election 2024'…",
534
+ label_visibility="collapsed")
535
+ with bcol:
536
+ fetch_btn = st.button("πŸ“‘ Fetch News", use_container_width=True)
537
+
538
+ if fetch_btn:
539
+ if not query.strip():
540
+ st.warning("Enter a search query.")
541
+ else:
542
+ with st.spinner(f"Fetching news for: **{query}**…"):
543
+ articles, err = fetch_news(query, NEWS_API_KEY)
544
+
545
+ if err:
546
+ st.error(f"NewsAPI error: {err}")
547
+ elif not articles:
548
+ st.info("No articles found. Try a different query.")
549
+ else:
550
+ results = []
551
+ progress = st.progress(0)
552
+ for i, art in enumerate(articles):
553
+ text = (art.get("title") or "") + " " + (art.get("description") or "")
554
+ if text.strip():
555
+ pred, conf, fp, rp = classify_text(text, tokenizer, model)
556
+ domain, cscore, clabel, ccolor = get_source_credibility(
557
+ art.get("url", ""))
558
+ indicators = detect_fake_indicators(text)
559
+ results.append({
560
+ "title": art.get("title", "No title"),
561
+ "source": art.get("source", {}).get("name", "Unknown"),
562
+ "url": art.get("url", "#"),
563
+ "publishedAt": art.get("publishedAt", ""),
564
+ "prediction": pred,
565
+ "confidence": conf,
566
+ "fake_prob": fp,
567
+ "real_prob": rp,
568
+ "cred_score": cscore,
569
+ "cred_label": clabel,
570
+ "indicators": indicators,
571
+ })
572
+ progress.progress((i + 1) / len(articles))
573
+ progress.empty()
574
+
575
+ # Summary metrics
576
+ fake_count = sum(1 for r in results if r["prediction"] == "FAKE")
577
+ real_count = len(results) - fake_count
578
+ avg_conf = np.mean([r["confidence"] for r in results]) * 100
579
+
580
+ st.markdown("---")
581
+ st.markdown('<div class="section-title">Batch Analysis Summary</div>',
582
+ unsafe_allow_html=True)
583
+ sm1, sm2, sm3, sm4 = st.columns(4)
584
+ with sm1:
585
+ st.markdown(f'<div class="metric-box"><div class="val">{len(results)}</div><div class="lbl">ARTICLES</div></div>',
586
+ unsafe_allow_html=True)
587
+ with sm2:
588
+ st.markdown(f'<div class="metric-box"><div class="val" style="color:#ef4444">{fake_count}</div><div class="lbl">FLAGGED FAKE</div></div>',
589
+ unsafe_allow_html=True)
590
+ with sm3:
591
+ st.markdown(f'<div class="metric-box"><div class="val" style="color:#22c55e">{real_count}</div><div class="lbl">LIKELY REAL</div></div>',
592
+ unsafe_allow_html=True)
593
+ with sm4:
594
+ st.markdown(f'<div class="metric-box"><div class="val">{avg_conf:.0f}%</div><div class="lbl">AVG CONFIDENCE</div></div>',
595
+ unsafe_allow_html=True)
596
+
597
+ # Batch chart
598
+ st.markdown("<br>", unsafe_allow_html=True)
599
+ titles_short = [r["title"][:45] + "…" if len(r["title"]) > 45 else r["title"]
600
+ for r in results]
601
+ colors = ["#ef4444" if r["prediction"] == "FAKE" else "#22c55e" for r in results]
602
+ fig_batch = go.Figure(go.Bar(
603
+ y=titles_short,
604
+ x=[r["fake_prob"] * 100 for r in results],
605
+ orientation="h",
606
+ marker_color=colors,
607
+ text=[f"{r['fake_prob']*100:.0f}%" for r in results],
608
+ textposition="outside",
609
+ textfont=dict(color="#e2e8f0", size=11),
610
+ ))
611
+ fig_batch.update_layout(
612
+ paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
613
+ xaxis=dict(range=[0, 120], ticksuffix="%", gridcolor="#1e293b",
614
+ tickfont=dict(color="#64748b")),
615
+ yaxis=dict(tickfont=dict(color="#e2e8f0", size=11)),
616
+ height=max(300, len(results) * 55),
617
+ margin=dict(t=10, b=10, l=10, r=80),
618
+ title=dict(text="Fake Probability per Article",
619
+ font=dict(color="#94a3b8", size=13)),
620
+ )
621
+ st.plotly_chart(fig_batch, use_container_width=True,
622
+ config={"displayModeBar": False})
623
+
624
+ # Individual cards
625
+ st.markdown('<div class="section-title">Individual Article Results</div>',
626
+ unsafe_allow_html=True)
627
+ for r in results:
628
+ badge_color = "#ef4444" if r["prediction"] == "FAKE" else "#22c55e"
629
+ badge_text = "⚠ FAKE" if r["prediction"] == "FAKE" else "βœ… REAL"
630
+ ind_html = "".join(
631
+ f'<span class="indicator-pill">{ind}</span>'
632
+ for ind in r["indicators"][:2]
633
+ ) if r["indicators"] else ""
634
+ st.markdown(f"""
635
+ <div class="news-card">
636
+ <div style="display:flex;justify-content:space-between;align-items:flex-start">
637
+ <h4>{r['title']}</h4>
638
+ <span style="background:{badge_color}22;color:{badge_color};
639
+ border:1px solid {badge_color};border-radius:99px;
640
+ padding:0.2rem 0.8rem;font-size:0.8rem;font-weight:700;
641
+ white-space:nowrap;margin-left:1rem">{badge_text}</span>
642
+ </div>
643
+ <p>πŸ“° {r['source']} &nbsp;Β·&nbsp; Confidence: {r['confidence']*100:.1f}%
644
+ &nbsp;Β·&nbsp; Source credibility: {r['cred_label']}</p>
645
+ {ind_html}
646
+ <p style="margin-top:0.5rem"><a href="{r['url']}" target="_blank"
647
+ style="color:#6366f1;font-size:0.8rem">Read original β†’</a></p>
648
+ </div>""", unsafe_allow_html=True)
649
+
650
+ # ── Footer ──────────────────────────────────
651
+ st.markdown("---")
652
+ st.markdown(
653
+ '<p style="text-align:center;color:#334155;font-size:0.8rem">'
654
+ 'FakeScope Β· Powered by πŸ€— Transformers Β· For educational use only'
655
+ '</p>',
656
+ unsafe_allow_html=True,
657
+ )