Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import requests | |
| import torch | |
| import numpy as np | |
| import os | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification | |
| from datetime import datetime | |
| import pandas as pd | |
| import re | |
| from urllib.parse import urlparse | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # π API KEY β PASTE YOUR NewsAPI KEY HERE | |
| NEWS_API_KEY = os.getenv("NEWS_API_KEY") | |
| # Get your free key at https://newsapi.org/register | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Model: Pre-trained fine-tuned BERT for fake news | |
| # No training needed β loaded directly from HuggingFace Hub | |
| MODEL_NAME = "jy46604790/Fake-News-Bert-Detect" | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Source credibility database | |
| SOURCE_CREDIBILITY = { | |
| "bbc.com": 0.97, "bbc.co.uk": 0.97, | |
| "reuters.com": 0.96, "apnews.com": 0.95, | |
| "theguardian.com": 0.93, "nytimes.com": 0.92, | |
| "washingtonpost.com": 0.91, "npr.org": 0.92, | |
| "bloomberg.com": 0.90, "economist.com": 0.92, | |
| "ft.com": 0.91, "nature.com": 0.97, | |
| "science.org": 0.97, "who.int": 0.98, | |
| "cdc.gov": 0.97, "gov.uk": 0.94, | |
| "thehindu.com": 0.88, "ndtv.com": 0.82, | |
| "hindustantimes.com": 0.80, "timesofindia.com": 0.79, | |
| "cnn.com": 0.78, "foxnews.com": 0.65, | |
| "huffpost.com": 0.70, "buzzfeed.com": 0.62, | |
| "vice.com": 0.68, "vox.com": 0.74, | |
| "medium.com": 0.52, "substack.com": 0.50, | |
| "infowars.com": 0.05, "naturalnews.com": 0.08, | |
| "beforeitsnews.com": 0.06, "worldnewsdailyreport.com": 0.04, | |
| "empirenews.net": 0.04, "theonion.com": 0.10, | |
| } | |
| CREDIBILITY_LABELS = { | |
| (0.85, 1.0): ("π’ Highly Credible", "#22c55e"), | |
| (0.65, 0.85): ("π‘ Moderately Credible", "#f59e0b"), | |
| (0.40, 0.65): ("π Low Credibility", "#f97316"), | |
| (0.0, 0.40): ("π΄ Very Low / Known Misinformation", "#ef4444"), | |
| } | |
| FAKE_INDICATORS = [ | |
| (r'\b(SHOCKING|BOMBSHELL|BREAKING|EXCLUSIVE)\b', "ALL-CAPS sensational trigger words"), | |
| (r'(!{2,}|\?{2,})', "Excessive punctuation (!! or ??)"), | |
| (r'\b(they don\'t want you to know|mainstream media won\'t tell)\b', "Anti-establishment conspiracy framing"), | |
| (r'\b(miracle|cure|secret|censored|banned)\b', "Clickbait / pseudoscience language"), | |
| (r'\b(100%|proven fact|scientists hate)\b', "Overconfident absolute claims"), | |
| (r'(share before deleted|share before banned)', "Urgency/fear-of-censorship manipulation"), | |
| (r'\b(deep state|new world order|illuminati|cabal)\b', "Conspiracy theory terminology"), | |
| (r'\baccording to sources\b(?!.*\bnamed\b)', "Vague anonymous sourcing"), | |
| ] | |
| def load_model(): | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) | |
| model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME) | |
| model.eval() | |
| return tokenizer, model | |
| def classify_text(text, tokenizer, model): | |
| inputs = tokenizer(text, return_tensors="pt", truncation=True, | |
| max_length=512, padding=True) | |
| with torch.no_grad(): | |
| outputs = model(**inputs) | |
| probs = torch.softmax(outputs.logits, dim=1).squeeze().numpy() | |
| labels = model.config.id2label | |
| fake_idx = next((i for i, l in labels.items() if "fake" in l.lower() or "0" == str(i)), 0) | |
| real_idx = 1 - fake_idx | |
| fake_prob = float(probs[fake_idx]) | |
| real_prob = float(probs[real_idx]) | |
| prediction = "FAKE" if fake_prob > real_prob else "REAL" | |
| confidence = max(fake_prob, real_prob) | |
| return prediction, confidence, fake_prob, real_prob | |
| def get_source_credibility(url_or_domain): | |
| if not url_or_domain: | |
| return None, 0.5, "Unknown Source", "#94a3b8" | |
| try: | |
| domain = urlparse(url_or_domain).netloc.lower().replace("www.", "") | |
| except Exception: | |
| domain = url_or_domain.lower().replace("www.", "") | |
| if domain in SOURCE_CREDIBILITY: | |
| score = SOURCE_CREDIBILITY[domain] | |
| else: | |
| score = 0.45 | |
| if domain.endswith(".gov") or domain.endswith(".edu"): | |
| score = 0.90 | |
| elif domain.endswith(".org"): | |
| score = 0.65 | |
| label, color = "Unknown", "#94a3b8" | |
| for (low, high), (lbl, clr) in CREDIBILITY_LABELS.items(): | |
| if low <= score <= high: | |
| label, color = lbl, clr | |
| break | |
| return domain, score, label, color | |
| def detect_fake_indicators(text): | |
| found = [] | |
| for pattern, description in FAKE_INDICATORS: | |
| if re.search(pattern, text, re.IGNORECASE): | |
| found.append(description) | |
| return found | |
| def fetch_news(query, api_key, max_articles=6): | |
| if not api_key or api_key == "YOUR_NEWSAPI_KEY_HERE": | |
| return None, "β οΈ No API key provided. Add your NewsAPI key in app.py." | |
| url = ( | |
| f"https://newsapi.org/v2/everything?" | |
| f"q={requests.utils.quote(query)}&language=en&sortBy=publishedAt" | |
| f"&pageSize={max_articles}&apiKey={api_key}" | |
| ) | |
| try: | |
| resp = requests.get(url, timeout=10) | |
| data = resp.json() | |
| if data.get("status") != "ok": | |
| return None, data.get("message", "API error") | |
| return data.get("articles", []), None | |
| except Exception as e: | |
| return None, str(e) | |
| def make_confidence_gauge(fake_prob, real_prob): | |
| fig = go.Figure(go.Indicator( | |
| mode="gauge+number+delta", | |
| value=round(fake_prob * 100, 1), | |
| domain={"x": [0, 1], "y": [0, 1]}, | |
| title={"text": "Fake Probability %", "font": {"size": 18, "color": "#e2e8f0"}}, | |
| number={"font": {"size": 36, "color": "#f8fafc"}, "suffix": "%"}, | |
| gauge={ | |
| "axis": {"range": [0, 100], "tickcolor": "#64748b", | |
| "tickfont": {"color": "#94a3b8"}}, | |
| "bar": {"color": "#6366f1"}, | |
| "steps": [ | |
| {"range": [0, 30], "color": "#14532d"}, | |
| {"range": [30, 55], "color": "#713f12"}, | |
| {"range": [55, 100], "color": "#7f1d1d"}, | |
| ], | |
| "threshold": { | |
| "line": {"color": "#fbbf24", "width": 4}, | |
| "thickness": 0.85, | |
| "value": 50, | |
| }, | |
| }, | |
| )) | |
| fig.update_layout( | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| font={"color": "#e2e8f0"}, | |
| height=280, | |
| margin=dict(t=50, b=10, l=30, r=30), | |
| ) | |
| return fig | |
| def make_prob_bar(fake_prob, real_prob): | |
| fig = go.Figure() | |
| fig.add_trace(go.Bar( | |
| x=["FAKE", "REAL"], | |
| y=[fake_prob * 100, real_prob * 100], | |
| marker_color=["#ef4444", "#22c55e"], | |
| text=[f"{fake_prob*100:.1f}%", f"{real_prob*100:.1f}%"], | |
| textposition="outside", | |
| textfont=dict(color="#f8fafc", size=14), | |
| width=0.45, | |
| )) | |
| fig.update_layout( | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| font=dict(color="#e2e8f0"), | |
| yaxis=dict(range=[0, 115], gridcolor="#1e293b", ticksuffix="%", | |
| tickfont=dict(color="#64748b")), | |
| xaxis=dict(tickfont=dict(color="#e2e8f0", size=14)), | |
| height=260, | |
| margin=dict(t=10, b=10, l=10, r=10), | |
| showlegend=False, | |
| ) | |
| return fig | |
| def credibility_bar_chart(domain, score): | |
| fig = go.Figure(go.Bar( | |
| x=[score * 100], | |
| y=[domain or "Unknown"], | |
| orientation="h", | |
| marker=dict( | |
| color=score * 100, | |
| colorscale=[[0, "#ef4444"], [0.5, "#f59e0b"], [1, "#22c55e"]], | |
| cmin=0, cmax=100, | |
| ), | |
| text=[f"{score*100:.0f}/100"], | |
| textposition="outside", | |
| textfont=dict(color="#f8fafc"), | |
| )) | |
| fig.update_layout( | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| xaxis=dict(range=[0, 115], gridcolor="#1e293b", ticksuffix="", | |
| tickfont=dict(color="#64748b")), | |
| yaxis=dict(tickfont=dict(color="#e2e8f0", size=13)), | |
| height=120, | |
| margin=dict(t=5, b=5, l=10, r=60), | |
| ) | |
| return fig | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # STREAMLIT UI | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.set_page_config( | |
| page_title="FakeScope β AI News Verifier", | |
| page_icon="π", | |
| layout="wide", | |
| initial_sidebar_state="expanded", | |
| ) | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap'); | |
| html, body, [class*="css"] { | |
| font-family: 'Syne', sans-serif; | |
| background-color: #050a14; | |
| color: #e2e8f0; | |
| } | |
| .stApp { background: #050a14; } | |
| .hero { | |
| background: linear-gradient(135deg, #0f172a 0%, #1a0a2e 50%, #0f172a 100%); | |
| border: 1px solid #1e293b; | |
| border-radius: 20px; | |
| padding: 2.5rem 3rem; | |
| margin-bottom: 2rem; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .hero::before { | |
| content: ''; | |
| position: absolute; | |
| top: -60px; right: -60px; | |
| width: 300px; height: 300px; | |
| background: radial-gradient(circle, rgba(99,102,241,0.15) 0%, transparent 70%); | |
| border-radius: 50%; | |
| } | |
| .hero h1 { | |
| font-size: 3rem; font-weight: 800; | |
| background: linear-gradient(90deg, #818cf8, #c084fc, #f472b6); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| margin: 0; letter-spacing: -1px; | |
| } | |
| .hero p { color: #94a3b8; font-size: 1.05rem; margin-top: 0.5rem; margin-bottom: 0; } | |
| .card { | |
| background: #0f172a; | |
| border: 1px solid #1e293b; | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .verdict-fake { | |
| border: 2px solid #ef4444; | |
| background: linear-gradient(135deg, #1a0000, #0f172a); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| text-align: center; | |
| } | |
| .verdict-real { | |
| border: 2px solid #22c55e; | |
| background: linear-gradient(135deg, #001a00, #0f172a); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| text-align: center; | |
| } | |
| .verdict-label { | |
| font-size: 2.5rem; font-weight: 800; letter-spacing: 4px; | |
| } | |
| .fake-label { color: #ef4444; } | |
| .real-label { color: #22c55e; } | |
| .indicator-pill { | |
| display: inline-block; | |
| background: #1e1030; | |
| border: 1px solid #7c3aed; | |
| color: #c084fc; | |
| border-radius: 99px; | |
| padding: 0.3rem 0.9rem; | |
| font-size: 0.82rem; | |
| margin: 0.25rem; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .news-card { | |
| background: #0f172a; | |
| border: 1px solid #1e293b; | |
| border-radius: 12px; | |
| padding: 1.2rem; | |
| margin-bottom: 0.8rem; | |
| transition: border-color 0.2s; | |
| } | |
| .news-card:hover { border-color: #6366f1; } | |
| .news-card h4 { color: #e2e8f0; font-size: 0.95rem; margin: 0 0 0.4rem 0; } | |
| .news-card p { color: #64748b; font-size: 0.82rem; margin: 0; } | |
| section[data-testid="stSidebar"] { | |
| background: #080d1a; | |
| border-right: 1px solid #1e293b; | |
| } | |
| .stTextArea textarea, .stTextInput input { | |
| background: #0f172a !important; | |
| border: 1px solid #334155 !important; | |
| color: #e2e8f0 !important; | |
| border-radius: 10px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| .stButton > button { | |
| background: linear-gradient(135deg, #4f46e5, #7c3aed) !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 10px !important; | |
| font-family: 'Syne', sans-serif !important; | |
| font-weight: 700 !important; | |
| font-size: 1rem !important; | |
| padding: 0.6rem 2rem !important; | |
| transition: opacity 0.2s !important; | |
| width: 100%; | |
| } | |
| .stButton > button:hover { opacity: 0.85 !important; } | |
| .section-title { | |
| font-size: 0.75rem; font-weight: 700; letter-spacing: 3px; | |
| color: #6366f1; text-transform: uppercase; margin-bottom: 0.75rem; | |
| } | |
| .metric-box { | |
| background: #0f172a; | |
| border: 1px solid #1e293b; | |
| border-radius: 12px; | |
| padding: 1rem 1.2rem; | |
| text-align: center; | |
| } | |
| .metric-box .val { font-size: 1.8rem; font-weight: 800; color: #818cf8; } | |
| .metric-box .lbl { font-size: 0.75rem; color: #64748b; letter-spacing: 1px; margin-top: 2px; } | |
| div[data-testid="stMetricValue"] { color: #818cf8 !important; font-family: 'Syne', sans-serif !important; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββ Sidebar ββββββββββββββββββββββββββββββββββ | |
| with st.sidebar: | |
| st.markdown("## π FakeScope") | |
| st.markdown("---") | |
| mode = st.radio("**Mode**", ["π Paste Article / Text", "π Fetch Live News"]) | |
| st.markdown("---") | |
| st.markdown("**About the Model**") | |
| st.caption(f"`{MODEL_NAME}`") | |
| st.caption("Fine-tuned BERT β no local training required.") | |
| st.markdown("---") | |
| st.markdown("**Credibility DB**") | |
| st.caption(f"{len(SOURCE_CREDIBILITY)} known sources indexed.") | |
| st.markdown("---") | |
| st.caption("Built with π€ Transformers + Streamlit") | |
| # ββ Hero βββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <div class="hero"> | |
| <h1>π FakeScope</h1> | |
| <p>AI-powered fake news detector Β· BERT Β· Source Credibility Β· Real-time News Β· Explainability</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββ Load model βββββββββββββββββββββββββββββββ | |
| with st.spinner("β‘ Loading BERT model from HuggingFace (first run only)β¦"): | |
| try: | |
| tokenizer, model = load_model() | |
| st.success("β Model loaded successfully!", icon="π€") | |
| except Exception as e: | |
| st.error(f"Model load failed: {e}") | |
| st.stop() | |
| # ββββββββββββββββββββββββββββββββββββββββββββ | |
| # MODE 1 β Paste Text | |
| # ββββββββββββββββββββββββββββββββββββββββββββ | |
| if mode == "π Paste Article / Text": | |
| st.markdown('<div class="section-title">Paste news article or headline</div>', unsafe_allow_html=True) | |
| col_in, col_meta = st.columns([3, 1]) | |
| with col_in: | |
| news_text = st.text_area("", height=180, | |
| placeholder="Paste a news headline, paragraph, or full article hereβ¦", | |
| label_visibility="collapsed") | |
| with col_meta: | |
| source_url = st.text_input("Source URL (optional)", | |
| placeholder="https://bbc.com/β¦") | |
| analyze_btn = st.button("π Analyze", use_container_width=True) | |
| if analyze_btn: | |
| if not news_text.strip(): | |
| st.warning("Please paste some text to analyze.") | |
| else: | |
| with st.spinner("Running BERT inferenceβ¦"): | |
| prediction, confidence, fake_prob, real_prob = classify_text( | |
| news_text, tokenizer, model) | |
| indicators = detect_fake_indicators(news_text) | |
| domain, cred_score, cred_label, cred_color = get_source_credibility(source_url) | |
| # ββ Verdict ββββββββββββββββββββββββββββββ | |
| st.markdown("---") | |
| vcol1, vcol2, vcol3 = st.columns([1, 2, 1]) | |
| with vcol2: | |
| if prediction == "FAKE": | |
| low_conf = confidence < 0.75 | |
| warning = ( | |
| "<div style='color:#fbbf24;font-size:0.85rem;margin-top:0.5rem'>" | |
| "β Low confidence β verify manually before concluding</div>" | |
| if low_conf else "" | |
| ) | |
| st.markdown( | |
| f""" | |
| <div class="verdict-fake"> | |
| <div class="verdict-label fake-label">β FAKE NEWS</div> | |
| <div style="color:#94a3b8;margin-top:0.4rem;font-size:0.95rem;"> | |
| Confidence: <b style="color:#f8fafc">{confidence*100:.1f}%</b> | |
| </div> | |
| {warning} | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| else: | |
| st.markdown( | |
| f""" | |
| <div class="verdict-real"> | |
| <div class="verdict-label real-label">β LIKELY REAL</div> | |
| <div style="color:#94a3b8;margin-top:0.4rem;font-size:0.95rem;"> | |
| Confidence: <b style="color:#f8fafc">{confidence*100:.1f}%</b> | |
| </div> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| # ββ Charts βββββββββββββββββββββββββββββββ | |
| ch1, ch2 = st.columns(2) | |
| with ch1: | |
| st.markdown('<div class="section-title">Confidence Gauge</div>', unsafe_allow_html=True) | |
| st.plotly_chart(make_confidence_gauge(fake_prob, real_prob), | |
| use_container_width=True, config={"displayModeBar": False}) | |
| with ch2: | |
| st.markdown('<div class="section-title">Probability Distribution</div>', unsafe_allow_html=True) | |
| st.plotly_chart(make_prob_bar(fake_prob, real_prob), | |
| use_container_width=True, config={"displayModeBar": False}) | |
| # ββ Source Credibility βββββββββββββββββββ | |
| st.markdown('<div class="section-title">Source Credibility Score</div>', unsafe_allow_html=True) | |
| st.markdown( | |
| f""" | |
| <div class="card"> | |
| <span style="font-size:1.1rem">{cred_label}</span> | |
| <span style="color:#64748b;font-family:monospace;font-size:0.85rem;margin-left:1rem"> | |
| {domain or 'Unknown domain'} | |
| </span> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.plotly_chart(credibility_bar_chart(domain or "Unknown", cred_score), | |
| use_container_width=True, config={"displayModeBar": False}) | |
| # ββ Why it might be fake βββββββββββββββββ | |
| st.markdown('<div class="section-title">π§ Explanation β Why it may be Fake</div>', | |
| unsafe_allow_html=True) | |
| with st.container(): | |
| if indicators: | |
| st.markdown("**Linguistic red flags detected:**") | |
| pills_html = "".join( | |
| f'<span class="indicator-pill">β {i}</span>' for i in indicators) | |
| st.markdown(pills_html, unsafe_allow_html=True) | |
| else: | |
| st.success("No obvious linguistic red flags detected in the text.") | |
| if prediction == "FAKE": | |
| reasons = [] | |
| if fake_prob > 0.85: | |
| reasons.append("Very high BERT fake-probability score (>85%)") | |
| if cred_score < 0.5: | |
| reasons.append( | |
| f"Source '{domain}' has very low credibility ({cred_score*100:.0f}/100)") | |
| if indicators: | |
| reasons.append( | |
| f"{len(indicators)} sensational/clickbait linguistic patterns found") | |
| if reasons: | |
| st.markdown("**Key reasons for FAKE classification:**") | |
| for r in reasons: | |
| st.markdown(f" πΈ {r}") | |
| # ββ Stats ββββββββββββββββββββββββββββββββ | |
| st.markdown('<div class="section-title">Analytics Summary</div>', unsafe_allow_html=True) | |
| m1, m2, m3, m4 = st.columns(4) | |
| with m1: | |
| st.markdown( | |
| f'<div class="metric-box"><div class="val">{fake_prob*100:.0f}%</div>' | |
| f'<div class="lbl">FAKE PROB</div></div>', | |
| unsafe_allow_html=True) | |
| with m2: | |
| st.markdown( | |
| f'<div class="metric-box"><div class="val">{real_prob*100:.0f}%</div>' | |
| f'<div class="lbl">REAL PROB</div></div>', | |
| unsafe_allow_html=True) | |
| with m3: | |
| st.markdown( | |
| f'<div class="metric-box"><div class="val">{cred_score*100:.0f}</div>' | |
| f'<div class="lbl">SOURCE SCORE</div></div>', | |
| unsafe_allow_html=True) | |
| with m4: | |
| st.markdown( | |
| f'<div class="metric-box"><div class="val">{len(indicators)}</div>' | |
| f'<div class="lbl">RED FLAGS</div></div>', | |
| unsafe_allow_html=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββ | |
| # MODE 2 β Live News Feed | |
| # ββββββββββββββββββββββββββββββββββββββββββββ | |
| else: | |
| st.markdown('<div class="section-title">Fetch & analyze live news articles</div>', | |
| unsafe_allow_html=True) | |
| qcol, bcol = st.columns([4, 1]) | |
| with qcol: | |
| query = st.text_input("", placeholder="Search topic e.g. 'climate change', 'election 2024'β¦", | |
| label_visibility="collapsed") | |
| with bcol: | |
| fetch_btn = st.button("π‘ Fetch News", use_container_width=True) | |
| if fetch_btn: | |
| if not query.strip(): | |
| st.warning("Enter a search query.") | |
| else: | |
| with st.spinner(f"Fetching news for: **{query}**β¦"): | |
| articles, err = fetch_news(query, NEWS_API_KEY) | |
| if err: | |
| st.error(f"NewsAPI error: {err}") | |
| elif not articles: | |
| st.info("No articles found. Try a different query.") | |
| else: | |
| results = [] | |
| progress = st.progress(0) | |
| for i, art in enumerate(articles): | |
| text = (art.get("title") or "") + " " + (art.get("description") or "") | |
| if text.strip(): | |
| pred, conf, fp, rp = classify_text(text, tokenizer, model) | |
| domain, cscore, clabel, ccolor = get_source_credibility( | |
| art.get("url", "")) | |
| indicators = detect_fake_indicators(text) | |
| results.append({ | |
| "title": art.get("title", "No title"), | |
| "source": art.get("source", {}).get("name", "Unknown"), | |
| "url": art.get("url", "#"), | |
| "publishedAt": art.get("publishedAt", ""), | |
| "prediction": pred, | |
| "confidence": conf, | |
| "fake_prob": fp, | |
| "real_prob": rp, | |
| "cred_score": cscore, | |
| "cred_label": clabel, | |
| "indicators": indicators, | |
| }) | |
| progress.progress((i + 1) / len(articles)) | |
| progress.empty() | |
| fake_count = sum(1 for r in results if r["prediction"] == "FAKE") | |
| real_count = len(results) - fake_count | |
| avg_conf = np.mean([r["confidence"] for r in results]) * 100 | |
| st.markdown("---") | |
| st.markdown('<div class="section-title">Batch Analysis Summary</div>', | |
| unsafe_allow_html=True) | |
| sm1, sm2, sm3, sm4 = st.columns(4) | |
| with sm1: | |
| st.markdown( | |
| f'<div class="metric-box"><div class="val">{len(results)}</div>' | |
| f'<div class="lbl">ARTICLES</div></div>', | |
| unsafe_allow_html=True) | |
| with sm2: | |
| st.markdown( | |
| f'<div class="metric-box"><div class="val" style="color:#ef4444">{fake_count}</div>' | |
| f'<div class="lbl">FLAGGED FAKE</div></div>', | |
| unsafe_allow_html=True) | |
| with sm3: | |
| st.markdown( | |
| f'<div class="metric-box"><div class="val" style="color:#22c55e">{real_count}</div>' | |
| f'<div class="lbl">LIKELY REAL</div></div>', | |
| unsafe_allow_html=True) | |
| with sm4: | |
| st.markdown( | |
| f'<div class="metric-box"><div class="val">{avg_conf:.0f}%</div>' | |
| f'<div class="lbl">AVG CONFIDENCE</div></div>', | |
| unsafe_allow_html=True) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| titles_short = [r["title"][:45] + "β¦" if len(r["title"]) > 45 else r["title"] | |
| for r in results] | |
| colors = ["#ef4444" if r["prediction"] == "FAKE" else "#22c55e" for r in results] | |
| fig_batch = go.Figure(go.Bar( | |
| y=titles_short, | |
| x=[r["fake_prob"] * 100 for r in results], | |
| orientation="h", | |
| marker_color=colors, | |
| text=[f"{r['fake_prob']*100:.0f}%" for r in results], | |
| textposition="outside", | |
| textfont=dict(color="#e2e8f0", size=11), | |
| )) | |
| fig_batch.update_layout( | |
| paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", | |
| xaxis=dict(range=[0, 120], ticksuffix="%", gridcolor="#1e293b", | |
| tickfont=dict(color="#64748b")), | |
| yaxis=dict(tickfont=dict(color="#e2e8f0", size=11)), | |
| height=max(300, len(results) * 55), | |
| margin=dict(t=10, b=10, l=10, r=80), | |
| title=dict(text="Fake Probability per Article", | |
| font=dict(color="#94a3b8", size=13)), | |
| ) | |
| st.plotly_chart(fig_batch, use_container_width=True, | |
| config={"displayModeBar": False}) | |
| st.markdown('<div class="section-title">Individual Article Results</div>', | |
| unsafe_allow_html=True) | |
| for r in results: | |
| badge_color = "#ef4444" if r["prediction"] == "FAKE" else "#22c55e" | |
| badge_text = "β FAKE" if r["prediction"] == "FAKE" else "β REAL" | |
| ind_html = "".join( | |
| f'<span class="indicator-pill">{ind}</span>' | |
| for ind in r["indicators"][:2] | |
| ) if r["indicators"] else "" | |
| st.markdown( | |
| f""" | |
| <div class="news-card"> | |
| <div style="display:flex;justify-content:space-between;align-items:flex-start"> | |
| <h4>{r['title']}</h4> | |
| <span style="background:{badge_color}22;color:{badge_color}; | |
| border:1px solid {badge_color};border-radius:99px; | |
| padding:0.2rem 0.8rem;font-size:0.8rem;font-weight:700; | |
| white-space:nowrap;margin-left:1rem">{badge_text}</span> | |
| </div> | |
| <p>π° {r['source']} Β· Confidence: {r['confidence']*100:.1f}% | |
| Β· Source credibility: {r['cred_label']}</p> | |
| {ind_html} | |
| <p style="margin-top:0.5rem"><a href="{r['url']}" target="_blank" | |
| style="color:#6366f1;font-size:0.8rem">Read original β</a></p> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| # ββ Footer βββββββββββββββββββββββββββββββββββ | |
| st.markdown("---") | |
| st.markdown( | |
| '<p style="text-align:center;color:#334155;font-size:0.8rem">' | |
| 'FakeScope Β· Powered by π€ Transformers Β· For educational use only' | |
| '</p>', | |
| unsafe_allow_html=True, | |
| ) |