bamaiyi1 commited on
Commit
613b0b2
·
verified ·
1 Parent(s): 0945c95

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +478 -0
  2. requirements.txt +18 -0
app.py ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ # Prevent transformers from importing torch (use TF-only loading)
3
+ os.environ.setdefault("TRANSFORMERS_NO_TORCH", "1")
4
+ os.environ.setdefault("HF_HUB_REQUEST_TIMEOUT", "120")
5
+
6
+ import io
7
+ import shutil
8
+ import streamlit as st
9
+ import tensorflow as tf
10
+ import numpy as np
11
+ import pandas as pd
12
+ import requests
13
+ import tensorflow as tf
14
+
15
+
16
+ from transformers import AutoTokenizer, TFAutoModelForSequenceClassification
17
+
18
+ # -----------------------------
19
+ # Styling (purple/blue glass theme)
20
+ # -----------------------------
21
+ st.set_page_config(page_title="📰 Fake News Dashboard", layout="wide", page_icon="🧠")
22
+
23
+ CSS = """
24
+ <style>
25
+ :root{
26
+ --card-bg: rgba(255,255,255,0.06);
27
+ --glass-bg: rgba(255,255,255,0.06);
28
+ --glass-border: rgba(255,255,255,0.08);
29
+ --accent: #4f46e5;
30
+ --muted: rgba(255,255,255,0.65);
31
+ }
32
+ body { background: linear-gradient(135deg,#0f172a 0%, #001219 100%); color: #e6eef8; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
33
+ .app-topbar { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 24px; }
34
+ .brand { display:flex; gap:12px; align-items:center; font-weight:700; font-size:20px; color: var(--accent); }
35
+ .icon-btn { background: transparent; border: none; color: var(--muted); cursor: pointer; font-size:18px; padding:8px; border-radius:10px; }
36
+ .icon-btn:hover { background: rgba(255,255,255,0.03); color: white; }
37
+ .card { background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); border:1px solid var(--glass-border); border-radius:14px; padding:18px; box-shadow: 0 6px 20px rgba(2,6,23,0.6); }
38
+ .kpi { display:flex; gap:14px; align-items:center; }
39
+ .kpi-value { font-size:20px; font-weight:700; color: var(--accent); }
40
+ .small { color: var(--muted); font-size:13px; }
41
+ .token-score { display:inline-block; margin:2px 4px; padding:6px 8px; border-radius:8px; background: rgba(255,255,255,0.02); font-size:12px; }
42
+ .stButton > button { background: linear-gradient(135deg, var(--accent), #2a2bd6); color: white; border: none; border-radius: 12px; padding: 12px 24px; font-weight:700; }
43
+ .stSidebar { background: linear-gradient(180deg, rgba(79,70,229,0.03), rgba(79,70,229,0.01)); border-right: 2px solid var(--glass-border); }
44
+ .grid { display:grid; grid-template-columns: repeat(3, 1fr); gap:16px; }
45
+ @media (max-width: 800px) {
46
+ .app-topbar { flex-direction:column; align-items:flex-start; gap:8px; }
47
+ .grid { grid-template-columns: 1fr; }
48
+ }
49
+ </style>
50
+ """
51
+ st.markdown(CSS, unsafe_allow_html=True)
52
+
53
+ # -----------------------------
54
+ # Load model & tokenizer (TF / default)
55
+ # -----------------------------
56
+ @st.cache_resource
57
+ def load_model(model_name="mrm8488/bert-tiny-finetuned-fake-news-detection"):
58
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
59
+ model = TFAutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2, from_pt=True)
60
+ return model, tokenizer
61
+
62
+ try:
63
+ model, tokenizer = load_model()
64
+ except Exception as e:
65
+ st.title("📰 Fake News Detector")
66
+ st.error("Failed to load model/tokenizer. See error below:")
67
+ st.code(repr(e))
68
+ st.stop()
69
+
70
+ # -----------------------------
71
+ # Prediction & helpers
72
+ # -----------------------------
73
+ def predict(text, model_arg=None, tokenizer_arg=None, max_length=128, return_attentions=None, return_att=None):
74
+ """
75
+ Backwards-compatible predict wrapper:
76
+ - accepts either (text, model, tokenizer, ...) or (text, return_att=...) calling styles
77
+ - uses module-level `model`/`tokenizer` if none provided
78
+ - accepts both `return_attentions` and legacy `return_att` keywords
79
+ """
80
+ # resolve model/tokenizer
81
+ mdl = model_arg if model_arg is not None else globals().get("model")
82
+ tok = tokenizer_arg if tokenizer_arg is not None else globals().get("tokenizer")
83
+ if mdl is None or tok is None:
84
+ raise RuntimeError("Model/tokenizer not available. Ensure load_model() succeeded.")
85
+
86
+ # resolve attention flag (support both names)
87
+ if return_att is None and return_attentions is None:
88
+ ra = False
89
+ elif return_att is None:
90
+ ra = bool(return_attentions)
91
+ else:
92
+ ra = bool(return_att)
93
+
94
+ inputs = tok(text, truncation=True, padding=True, return_tensors="tf", max_length=max_length)
95
+ outputs = mdl(**inputs, output_attentions=ra, training=False)
96
+ probs = tf.nn.softmax(outputs.logits, axis=-1).numpy()[0]
97
+ pred = int(np.argmax(probs))
98
+ attn = None
99
+ if ra and getattr(outputs, "attentions", None) is not None:
100
+ try:
101
+ attns = outputs.attentions
102
+ agg = None
103
+ for layer in attns:
104
+ arr = np.array(layer) # (batch, heads, seq, seq)
105
+ mean = arr.mean(axis=(0,1)) # (seq, seq)
106
+ col = mean[:, 0]
107
+ agg = col if agg is None else agg + col
108
+ scores = (agg / len(attns)).tolist()
109
+ tokens = tok.convert_ids_to_tokens(inputs["input_ids"].numpy()[0])
110
+ attn = list(zip(tokens, scores))
111
+ except Exception:
112
+ attn = None
113
+ return pred, float(np.max(probs)), float(probs[0]), float(probs[1]), attn
114
+
115
+ def infer_label_indices(model, tokenizer):
116
+ """
117
+ Heuristically infer which class index corresponds to 'FAKE' by scoring
118
+ a small calibration set of known fake/real headlines. Stores result in
119
+ session_state as 'label_index_for_fake' (0 or 1).
120
+ """
121
+ # small, obvious calibration examples
122
+ fake_examples = [
123
+ "Breaking: NASA confirms aliens landed in Times Square last night",
124
+ "Miracle pill cures all diseases, scientists stunned",
125
+ "Government to replace currency with pizza next month"
126
+ ]
127
+ real_examples = [
128
+ "Stock market closes up 1.5% on positive earnings reports",
129
+ "City council approves $50 million infrastructure budget",
130
+ "New climate report released by international scientific body"
131
+ ]
132
+
133
+ def avg_probs(texts):
134
+ probs_acc = np.zeros(2, dtype=float)
135
+ n = 0
136
+ for t in texts:
137
+ try:
138
+ inputs = tokenizer(t, truncation=True, padding=True, return_tensors="tf", max_length=128)
139
+ outputs = model(**inputs, training=False)
140
+ p = tf.nn.softmax(outputs.logits, axis=-1).numpy()[0]
141
+ probs_acc += p
142
+ n += 1
143
+ except Exception:
144
+ continue
145
+ return probs_acc / max(1, n)
146
+
147
+ fake_avg = avg_probs(fake_examples)
148
+ real_avg = avg_probs(real_examples)
149
+
150
+ # whichever index has higher mean probability on fake examples -> fake index
151
+ fake_index = int(np.argmax(fake_avg))
152
+ # store for later use
153
+ st.session_state["label_index_for_fake"] = fake_index
154
+ st.session_state["label_index_for_real"] = 1 - fake_index
155
+ return fake_index
156
+
157
+ # replace resolve_label_map/human_label_from_pred behavior with auto-detection
158
+ def human_label_from_pred(pred, model, invert=False):
159
+ """
160
+ Determine REAL/FAKE using model.config.id2label if explicit, otherwise
161
+ use an inferred mapping from a small calibration set and cache it.
162
+ """
163
+ # 1) try id2label mapping (explicit textual labels)
164
+ try:
165
+ cfg = getattr(model, "config", None)
166
+ if cfg and getattr(cfg, "id2label", None):
167
+ id2 = cfg.id2label
168
+ v = str(id2.get(pred, "")).lower()
169
+ if "fake" in v or "false" in v or "fraud" in v:
170
+ res = "FAKE"
171
+ elif "real" in v or "true" in v or "legit" in v:
172
+ res = "REAL"
173
+ else:
174
+ # fall back to inference below
175
+ raise ValueError("id2label ambiguous")
176
+ if invert:
177
+ res = "REAL" if res == "FAKE" else "FAKE"
178
+ return res
179
+ except Exception:
180
+ pass
181
+
182
+ # 2) use cached inference or run inference once
183
+ if "label_index_for_fake" not in st.session_state:
184
+ try:
185
+ infer_label_indices(model, tokenizer)
186
+ except Exception:
187
+ # last resort numeric default
188
+ res = "REAL" if pred == 1 else "FAKE"
189
+ if invert:
190
+ res = "REAL" if res == "FAKE" else "FAKE"
191
+ return res
192
+
193
+ fake_idx = st.session_state.get("label_index_for_fake", 1)
194
+ # map pred -> human label
195
+ res = "FAKE" if pred == fake_idx else "REAL"
196
+ if invert:
197
+ res = "REAL" if res == "FAKE" else "FAKE"
198
+ return res
199
+
200
+ # -----------------------------
201
+ # Sidebar navigation
202
+ # -----------------------------
203
+ st.sidebar.title("Navigation")
204
+ page = st.sidebar.radio("", ["Dashboard", "Single Predict", "Batch Predict", "Fetch & Predict", "About"], index=0)
205
+
206
+ # -----------------------------
207
+ # UI layout
208
+ # -----------------------------
209
+ st.title("📰 Fake News Detection — Dashboard")
210
+ max_len = st.sidebar.slider("Max tokens", 64, 512, 128, step=32)
211
+ show_attention = st.sidebar.checkbox("Show token importance", value=True)
212
+ invert_labels = st.sidebar.checkbox("Invert label mapping", value=False)
213
+
214
+ # -----------------------------
215
+ # Pages
216
+ # -----------------------------
217
+ if page == "Dashboard":
218
+ st.header("Dashboard")
219
+ st.markdown("**Examples**")
220
+ examples = [
221
+ "Breaking: Celebrity endorses miracle cure — doctors shocked",
222
+ "Government announces new infrastructure spending plan",
223
+ "Study shows chocolate linked with longer life"
224
+ ]
225
+ for ex in examples:
226
+ if st.button(f"🔎 {ex}", key=f"ex_{ex[:12]}"):
227
+ st.session_state["example_text"] = ex
228
+ st.markdown('</div>', unsafe_allow_html=True)
229
+
230
+ st.markdown('<div class="card">', unsafe_allow_html=True)
231
+ last = st.session_state.get("last_result", None)
232
+ if last:
233
+ st.markdown("**Last prediction**")
234
+ st.write(last["text"])
235
+ st.info(f'{last["prediction"]} — confidence {last["confidence"]:.2%}')
236
+ else:
237
+ st.markdown("**Last prediction**")
238
+ st.write("_No predictions yet_")
239
+ st.markdown('</div>', unsafe_allow_html=True)
240
+ st.markdown('</div>', unsafe_allow_html=True)
241
+
242
+ colA, colB = st.columns([2,1])
243
+ with colA:
244
+ st.markdown('<div class="card">', unsafe_allow_html=True)
245
+ example_val = st.session_state.get("example_text", "")
246
+ txt = st.text_area("Enter headline or article:", value=example_val, height=160, key="dash_input")
247
+ if st.button("Analyze (Dashboard)"):
248
+ pred, conf, p0, p1, att = predict(txt, model, tokenizer, max_length=max_len, return_attentions=show_attention)
249
+ label = human_label_from_pred(pred, model, invert=invert_labels)
250
+ st.write(f"raw probs: index0={p0:.3f}, index1={p1:.3f}")
251
+ st.success(f"{label} — confidence {conf:.2%}")
252
+ st.session_state["last_result"] = {"text": txt, "prediction": label, "confidence": conf, "att": att}
253
+ if att and show_attention:
254
+ st.write("Token importance:")
255
+ for t,s in att[:60]:
256
+ st.markdown(f"<span class='token-score' title='{s:.4f}'>{t}</span>", unsafe_allow_html=True)
257
+ st.markdown('</div>', unsafe_allow_html=True)
258
+ with colB:
259
+ st.markdown('<div class="card">', unsafe_allow_html=True)
260
+ st.subheader("Quick actions")
261
+ if st.button("Analyze example 1"):
262
+ st.session_state["example_text"] = examples[0]
263
+ st.markdown("Upload CSV for batch predictions in the Batch page.")
264
+ st.markdown('</div>', unsafe_allow_html=True)
265
+
266
+ elif page == "Single Predict":
267
+ st.header("Single Prediction")
268
+ st.markdown('<div class="card">', unsafe_allow_html=True)
269
+
270
+ # Input + optional NewsAPI key (reuses session key if set elsewhere)
271
+ input_text = st.text_area("Paste headline or article:", height=200)
272
+ api_key = st.text_input("NewsAPI key (optional — required for online source lookup)", type="password", key="newsapi_key")
273
+
274
+ # Lookup controls
275
+ lookup = st.checkbox("Lookup source online (NewsAPI)", value=False)
276
+ source_info = None
277
+ if lookup:
278
+ st.markdown("Use NewsAPI to find the likely source for this headline.")
279
+ if st.button("Find source", key="find_source"):
280
+ if not api_key:
281
+ st.warning("Enter a NewsAPI key to enable online lookup (get free key at https://newsapi.org).")
282
+ else:
283
+ try:
284
+ with st.spinner("Searching for source..."):
285
+ params = {
286
+ "qInTitle": input_text,
287
+ "apiKey": api_key,
288
+ "pageSize": 1,
289
+ "sortBy": "relevancy",
290
+ }
291
+ resp = requests.get("https://newsapi.org/v2/everything", params=params, timeout=10)
292
+ resp.raise_for_status()
293
+ data = resp.json()
294
+ articles = data.get("articles", [])
295
+ if articles:
296
+ a = articles[0]
297
+ source_info = {
298
+ "source": a.get("source", {}).get("name", ""),
299
+ "url": a.get("url", ""),
300
+ "publishedAt": a.get("publishedAt", "")
301
+ }
302
+ st.success(f"Found source: {source_info['source']}")
303
+ st.write(f"[Open article]({source_info['url']})")
304
+ else:
305
+ st.info("No matching article found for that headline.")
306
+ except Exception as e:
307
+ st.error(f"Source lookup failed: {e}")
308
+
309
+ # Analyze / Predict
310
+ if st.button("Analyze"):
311
+ if input_text.strip() == "":
312
+ st.warning("Enter text first.")
313
+ else:
314
+ with st.spinner("Predicting..."):
315
+ pred, conf, p_fake, p_real, att = predict(input_text, return_att=show_attention)
316
+
317
+ label = human_label_from_pred(pred, model, invert=invert_labels)
318
+ st.write(f"raw probs: index0={p_fake:.3f}, index1={p_real:.3f}")
319
+ st.success(f"{label} — confidence {conf:.2%}")
320
+
321
+ # show detected source if available
322
+ if source_info:
323
+ st.info(f"Detected source: **{source_info['source']}** — [Open article]({source_info['url']})")
324
+ if source_info.get("publishedAt"):
325
+ st.caption(f"Published at: {source_info['publishedAt']}")
326
+
327
+ st.metric("P(Real)", f"{p_real:.2%}")
328
+ st.metric("P(Fake)", f"{p_fake:.2%}")
329
+
330
+ st.session_state["last_result"] = {
331
+ "text": input_text,
332
+ "prediction": label,
333
+ "confidence": conf,
334
+ "att": att,
335
+ "source": source_info
336
+ }
337
+
338
+ if show_attention and att:
339
+ st.write("Token importance (top tokens):")
340
+ df_att = pd.DataFrame(att, columns=["token", "score"]).head(60)
341
+ st.table(df_att)
342
+
343
+ st.markdown('</div>', unsafe_allow_html=True)
344
+
345
+ elif page == "Batch Predict":
346
+ st.header("Batch Prediction")
347
+ st.markdown('<div class="card">', unsafe_allow_html=True)
348
+ uploaded = st.file_uploader("Upload CSV or TXT (one text per line)", type=["csv","txt"])
349
+ if uploaded is not None:
350
+ raw = uploaded.getvalue()
351
+ try:
352
+ decoded = raw.decode("utf-8", errors="replace")
353
+ lines = [l.strip() for l in decoded.splitlines() if l.strip()]
354
+ st.info(f"Parsed {len(lines)} records.")
355
+ if st.button("Run batch"):
356
+ results = []
357
+ progress = st.progress(0)
358
+ for i, txt in enumerate(lines):
359
+ pred, conf, p0, p1, _ = predict(txt, model, tokenizer, max_length=max_len, return_attentions=False)
360
+ mapped = human_label_from_pred(pred, model, invert=invert_labels)
361
+ results.append({"text": txt, "prediction": mapped, "confidence": conf, "p_fake": p0, "p_real": p1})
362
+ progress.progress((i+1)/len(lines))
363
+ df = pd.DataFrame(results); st.dataframe(df, use_container_width=True)
364
+ csv_bytes = df.to_csv(index=False).encode("utf-8"); st.download_button("Download CSV", data=csv_bytes, file_name="batch_predictions.csv", mime="text/csv")
365
+ if len(results)>0: st.session_state["last_result"] = results[0]
366
+ except Exception as e:
367
+ st.error(f"Failed to parse file: {e}")
368
+ st.markdown('</div>', unsafe_allow_html=True)
369
+
370
+ elif page == "Fetch & Predict":
371
+ st.header("Fetch & Predict from Web")
372
+ st.markdown('<div class="card">', unsafe_allow_html=True)
373
+ st.write("Fetch real news headlines from the web and predict if they are fake or real.")
374
+
375
+ # NewsAPI key input
376
+ api_key = st.text_input("NewsAPI key (get free key at https://newsapi.org)", type="password", key="newsapi_key")
377
+
378
+ col1, col2 = st.columns(2)
379
+ with col1:
380
+ query = st.text_input("Search query (e.g., 'technology', 'politics')", value="technology")
381
+ with col2:
382
+ num_articles = st.slider("Number of articles to fetch", 1, 50, 10)
383
+
384
+ if st.button("Fetch & Analyze"):
385
+ if not api_key:
386
+ st.error("Please enter a NewsAPI key. Get one free at https://newsapi.org")
387
+ else:
388
+ try:
389
+ with st.spinner("Fetching articles from NewsAPI..."):
390
+ url = "https://newsapi.org/v2/everything"
391
+ params = {
392
+ "q": query,
393
+ "apiKey": api_key,
394
+ "pageSize": num_articles,
395
+ "sortBy": "publishedAt"
396
+ }
397
+ response = requests.get(url, params=params, timeout=10)
398
+ response.raise_for_status()
399
+ data = response.json()
400
+
401
+ if data.get("status") != "ok":
402
+ st.error(f"API Error: {data.get('message', 'Unknown error')}")
403
+ else:
404
+ articles = data.get("articles", [])
405
+ st.success(f"Fetched {len(articles)} articles. Analyzing...")
406
+
407
+ results = []
408
+ progress = st.progress(0)
409
+
410
+ for i, article in enumerate(articles):
411
+ title = article.get("title", "")
412
+ description = article.get("description", "") or ""
413
+ source = article.get("source", {}).get("name", "Unknown")
414
+ url_article = article.get("url", "")
415
+
416
+ # Predict on title + description
417
+ text_to_predict = f"{title}. {description}"
418
+ if text_to_predict.strip():
419
+ pred, conf, p_fake, p_real, _ = predict(text_to_predict, return_att=False)
420
+ label = human_label_from_pred(pred, model, invert=invert_labels)
421
+ results.append({
422
+ "title": title,
423
+ "source": source,
424
+ "prediction": label,
425
+ "confidence": conf,
426
+ "p_fake": p_fake,
427
+ "p_real": p_real,
428
+ "url": url_article
429
+ })
430
+ progress.progress((i + 1) / len(articles))
431
+
432
+ # Display results
433
+ df = pd.DataFrame(results)
434
+ st.subheader(f"Results ({len(results)} articles)")
435
+
436
+ # Color-code by prediction
437
+ def highlight_prediction(row):
438
+ if row["prediction"] == "FAKE":
439
+ return ["background-color: #ff6b6b"] * len(row)
440
+ else:
441
+ return ["background-color: #51cf66"] * len(row)
442
+
443
+ st.dataframe(
444
+ df[["title", "source", "prediction", "confidence"]].style.apply(highlight_prediction, axis=1),
445
+ use_container_width=True
446
+ )
447
+
448
+ # Download button
449
+ csv_bytes = df.to_csv(index=False).encode("utf-8")
450
+ st.download_button("Download results CSV", data=csv_bytes, file_name="web_predictions.csv", mime="text/csv")
451
+
452
+ # Show article links
453
+ st.subheader("Articles")
454
+ for idx, row in df.iterrows():
455
+ emoji = "🔴" if row["prediction"] == "FAKE" else "🟢"
456
+ st.write(f"{emoji} [{row['title']}]({row['url']}) — {row['source']}")
457
+ st.caption(f"Confidence: {row['confidence']:.2%}")
458
+
459
+ except requests.exceptions.RequestException as e:
460
+ st.error(f"Failed to fetch articles: {e}")
461
+ except Exception as e:
462
+ st.error(f"Error: {e}")
463
+
464
+ st.markdown('</div>', unsafe_allow_html=True)
465
+
466
+ else: # About
467
+ st.header("About")
468
+ st.markdown('<div class="card">', unsafe_allow_html=True)
469
+ st.markdown("""
470
+ **Fake News Detector** — dashboard UI built with Streamlit.
471
+ - Uses BERT fine-tuned (mrm8488/bert-tiny-finetuned-fake-news-detection) for classification (TF).
472
+ - Sidebar navigation, top taskbar, glass cards, icon buttons.
473
+ - Single and batch prediction pages.
474
+ """)
475
+ st.markdown('</div>', unsafe_allow_html=True)
476
+
477
+ st.markdown("---")
478
+ st.caption("Built with ❤️ — Streamlit + Transformers. Ensure Streamlit runs in same Python env as installed packages. adeyi bamaiyi. thanks mr steve, thanks torbita. love u guys ")
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ tensorflow
3
+ transformers==4.57.3
4
+ pandas
5
+ numpy
6
+ beautifulsoup4
7
+ requests
8
+ scikit-learn
9
+ pytest
10
+ python-dotenv
11
+ regex
12
+ pyyaml
13
+ huggingface-hub
14
+ safetensors
15
+ tqdm
16
+ filelock
17
+ packaging
18
+ tokenizers