bamaiyi1 commited on
Commit
2384bbd
Β·
verified Β·
1 Parent(s): 5d709a7

Upload app.py

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