Arko007 commited on
Commit
e4ebc28
·
verified ·
1 Parent(s): 583611b

Rework pipeline (dual models + Tavily + LLM final verdict), SaaS UI, fix torchvision logs

Browse files
Files changed (4) hide show
  1. .streamlit/config.toml +2 -1
  2. README.md +20 -35
  3. requirements.txt +1 -0
  4. src/streamlit_app.py +592 -581
.streamlit/config.toml CHANGED
@@ -1,13 +1,14 @@
1
  [theme]
2
  base = "light"
3
  primaryColor = "#6366f1"
4
- backgroundColor = "#f5f7ff"
5
  secondaryBackgroundColor = "#ffffff"
6
  textColor = "#1e293b"
7
  font = "sans serif"
8
 
9
  [server]
10
  headless = true
 
11
 
12
  [browser]
13
  gatherUsageStats = false
 
1
  [theme]
2
  base = "light"
3
  primaryColor = "#6366f1"
4
+ backgroundColor = "#f6f8fc"
5
  secondaryBackgroundColor = "#ffffff"
6
  textColor = "#1e293b"
7
  font = "sans serif"
8
 
9
  [server]
10
  headless = true
11
+ fileWatcherType = "none"
12
 
13
  [browser]
14
  gatherUsageStats = false
README.md CHANGED
@@ -6,59 +6,44 @@ colorTo: purple
6
  sdk: docker
7
  app_port: 8501
8
  pinned: false
9
- short_description: The Two-Brain Misinformation Detector
10
  license: mit
11
  ---
12
 
13
- # 🧠 Credo AI: The Two-Brain Misinformation Detector
14
 
15
- Welcome to the live demo of **Credo AI** a "Two-Brain" AI system that provides rapid and in-depth analysis of news articles and text, built for the Hack2Skill Hackathon 2025.
16
 
17
  **Live Space:** https://huggingface.co/spaces/Arko007/Credo_AI
18
 
19
- ## Key Features
20
 
21
- - **Live URL & Text Analysis:** Paste any text or a URL to a news article for an instant analysis.
22
- - **Dual-AI Verdict:** A fast, high-confidence FAKE/REAL verdict from a specialist model, plus a nuanced check from an expert model.
23
- - **Groq-Powered Explanations:** Clear, conversational explanations of the findings, powered by Groq LPU inference (`qwen/qwen3.6-27b`, fallback `openai/gpt-oss-120b`).
24
- - **Web Cross-Reference:** Optional Tavily-based search to cross-check claims against live sources.
25
- - **Analysis History:** All queries are saved in your session and reviewable on the History page.
26
- - **Export:** Download each analysis as a JSON report.
27
 
28
- ## Models
29
 
30
- | Brain | Model | Base | Role |
31
- |---|---|---|---|
32
- | Brain 1 | `Arko007/fake-news-liar-political` | RoBERTa-base | Political (US-centric) FAKE/REAL |
33
- | Brain 2 | `Arko007/fact-check1-v3-final` | DeBERTa-v3-large | General FAKE/REAL specialist |
34
 
35
- The app automatically routes political text to Brain 1 and everything else to Brain 2.
 
 
 
 
36
 
37
  ## Setup
38
 
39
  ### Secrets (Space Settings → Variables and Secrets)
40
 
41
- - `GROQ_API_KEY` — required for AI-powered explanations (https://console.groq.com)
42
- - `TAVILY_API_KEY` — optional, enables live web cross-referencing (https://tavily.com)
43
 
44
- The app works in Basic Mode without any keys, using built-in summaries and fallback analysis.
45
 
46
- ## ⚠️ Important Limitations
47
 
48
- This is a hackathon prototype. The models are expert **pattern detectors**, not universal truth engines:
49
-
50
- 1. **Pattern Recognition vs. World Knowledge** — simple, declarative but factually false statements (e.g., "The sun rises in the West") can be missed because they don't match fake-news stylistic patterns.
51
- 2. **Limited Training Data & Domain Bias** — models were trained mostly on Western political news; accuracy drops on science, finance, health, and non-Western news contexts.
52
- 3. **Future Work** — knowledge-graph grounding, multi-domain training, and global datasets.
53
-
54
- Always treat results as assistive, not authoritative.
55
-
56
- ## Tech Stack
57
-
58
- - **Frontend:** Streamlit + custom CSS
59
- - **Models:** Hugging Face Transformers (PyTorch), RoBERTa & DeBERTa-v3
60
- - **Explanations:** Groq Cloud API
61
- - **Search:** Tavily
62
- - **Scraping:** Beautiful Soup + lxml
63
 
64
  Built by the Data Dragons 🐉 for the Hack2Skill Hackathon 2025.
 
6
  sdk: docker
7
  app_port: 8501
8
  pinned: false
9
+ short_description: Fact-check any claim in seconds
10
  license: mit
11
  ---
12
 
13
+ # 🧠 Credo Truth, verified.
14
 
15
+ **Credo** is a consumer-friendly fact-checking app. Paste a claim, headline, article, or a link get a clear **Real / Fake / Unclear** verdict with confidence, key takeaways, and sources.
16
 
17
  **Live Space:** https://huggingface.co/spaces/Arko007/Credo_AI
18
 
19
+ ## How the pipeline works
20
 
21
+ 1. **Content extraction** direct text, URL scraping (Beautiful Soup + lxml), or file upload.
22
+ 2. **AI cross-check** the content is analyzed by two specialized classifiers (a general-purpose one and a political one).
23
+ 3. **Web cross-reference** Tavily searches live web sources for related evidence.
24
+ 4. **Final verdict** an LLM on Groq LPU hardware (qwen/qwen3.6-27b, fallback gpt-oss-120b / gpt-oss-20b) reviews the classifiers and the web evidence and issues the final verdict, explanation, and key points. The LLM is the final judge and its verdict overrides the classifiers.
 
 
25
 
26
+ Users only ever see the final verdict — internal model details are never exposed.
27
 
28
+ ## Features
 
 
 
29
 
30
+ - Clean, lightweight SaaS-style UI
31
+ - Verdicts with confidence levels (High / Medium / Low) and a confidence bar
32
+ - Key takeaways and source links with every result
33
+ - Full analysis history and one-click JSON report export
34
+ - Graceful fallback to model-only analysis if the LLM is unavailable
35
 
36
  ## Setup
37
 
38
  ### Secrets (Space Settings → Variables and Secrets)
39
 
40
+ - `GROQ_API_KEY` — required for final LLM verdicts and explanations (https://console.groq.com)
41
+ - `TAVILY_API_KEY` — enables live web cross-referencing (https://tavily.com)
42
 
43
+ The app works without keys using model-only fallback analysis.
44
 
45
+ ## ⚠️ Honest limitations
46
 
47
+ Credo is a helpful first filter, not a perfect truth machine. AI pattern detection works best on news-style text and can miss false claims that look completely normal. Always verify important information with trusted sources before sharing.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  Built by the Data Dragons 🐉 for the Hack2Skill Hackathon 2025.
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  streamlit>=1.46.0
2
  transformers>=4.57.0
3
  torch>=2.5.0
 
4
  groq>=0.14.0
5
  tavily-python>=0.5.0
6
  pandas>=2.2.0
 
1
  streamlit>=1.46.0
2
  transformers>=4.57.0
3
  torch>=2.5.0
4
+ torchvision>=0.21.0
5
  groq>=0.14.0
6
  tavily-python>=0.5.0
7
  pandas>=2.2.0
src/streamlit_app.py CHANGED
@@ -1,7 +1,6 @@
1
  import os
2
- import random
3
- import time
4
  import json
 
5
  import requests
6
  from datetime import datetime
7
  from bs4 import BeautifulSoup
@@ -26,8 +25,8 @@ except Exception:
26
  TAVILY_CLIENT = None
27
  TAVILY_AVAILABLE = False
28
 
29
- BRAIN_1_MODEL = "Arko007/fake-news-liar-political"
30
- BRAIN_2_MODEL = "Arko007/fact-check1-v3-final"
31
 
32
  GROQ_MODELS = [
33
  "qwen/qwen3.6-27b",
@@ -35,8 +34,10 @@ GROQ_MODELS = [
35
  "openai/gpt-oss-20b",
36
  ]
37
 
 
 
38
  st.set_page_config(
39
- page_title="Credo AI | Truth Detection Platform",
40
  page_icon="🧠",
41
  layout="wide",
42
  initial_sidebar_state="expanded",
@@ -44,19 +45,18 @@ st.set_page_config(
44
 
45
  st.markdown("""
46
  <style>
47
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
48
-
49
  html, body, [class*="css"], [class*="st-"], .stApp {
50
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
 
51
  }
52
 
53
  .stApp {
54
- background: linear-gradient(135deg, #f5f7ff 0%, #eef1fb 50%, #e9eefa 100%);
55
  color: #1e293b;
56
  }
57
 
58
  h1, h2, h3, h4 {
59
- color: #1e293b;
60
  letter-spacing: -0.01em;
61
  }
62
 
@@ -68,144 +68,222 @@ h1, h2, h3, h4 {
68
  color: #475569;
69
  }
70
 
71
- .hero-container {
72
- background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
73
- border: 1px solid #e5e9f2;
74
- border-radius: 20px;
75
- padding: 2.5rem 2rem;
76
  text-align: center;
77
- box-shadow: 0 10px 30px rgba(99, 102, 241, 0.08);
78
- margin-bottom: 1.5rem;
 
 
 
79
  }
80
- .main-title {
81
- background: linear-gradient(90deg, #4f46e5, #7c3aed, #4f46e5);
82
- background-size: 200% auto;
 
 
83
  -webkit-background-clip: text;
84
  -webkit-text-fill-color: transparent;
85
  background-clip: text;
86
- animation: shimmer 6s linear infinite;
87
- font-size: 2.6rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  font-weight: 800;
 
 
89
  letter-spacing: -0.02em;
90
- margin: 0 0 0.75rem 0;
91
  }
92
- @keyframes shimmer {
93
- to { background-position: 200% center; }
 
 
 
94
  }
95
- .hero-subtitle {
96
  color: #64748b;
97
  font-size: 1.1rem;
98
- max-width: 760px;
99
- margin: 0 auto 1.5rem auto;
100
- line-height: 1.7;
101
  }
102
- .metrics-container {
 
103
  display: flex;
104
- gap: 1rem;
105
  justify-content: center;
106
  flex-wrap: wrap;
 
107
  }
108
- .metric-card {
109
  background: #ffffff;
110
  border: 1px solid #e5e9f2;
111
- border-radius: 16px;
112
- padding: 1rem 1.75rem;
113
- min-width: 130px;
114
- box-shadow: 0 6px 16px rgba(99, 102, 241, 0.06);
115
- transition: transform 0.2s ease, box-shadow 0.2s ease;
116
- }
117
- .metric-card:hover {
118
- transform: translateY(-3px);
119
- box-shadow: 0 10px 24px rgba(99, 102, 241, 0.14);
120
- }
121
- .metric-value {
122
- display: block;
123
- font-size: 1.9rem;
124
- font-weight: 800;
125
- background: linear-gradient(90deg, #4f46e5, #7c3aed);
126
- -webkit-background-clip: text;
127
- -webkit-text-fill-color: transparent;
128
- background-clip: text;
129
- }
130
- .metric-label {
131
- display: block;
132
- color: #94a3b8;
133
- font-size: 0.78rem;
134
- text-transform: uppercase;
135
- letter-spacing: 0.08em;
136
- margin-top: 0.2rem;
137
  }
138
 
139
- .summary-box {
140
  background: #ffffff;
141
  border: 1px solid #e5e9f2;
142
- border-left: 5px solid #6366f1;
143
- border-radius: 14px;
144
- padding: 1.25rem 1.5rem;
145
- color: #334155;
146
- line-height: 1.7;
147
- font-size: 1.02rem;
148
- box-shadow: 0 6px 18px rgba(99, 102, 241, 0.06);
149
  }
150
 
151
- .verdict-container {
152
- border-radius: 18px;
153
- padding: 2rem 1.5rem;
154
- text-align: center;
155
- box-shadow: 0 10px 26px rgba(15, 23, 42, 0.12);
 
156
  }
157
- .verdict-fake {
158
- background: linear-gradient(135deg, #ef4444, #dc2626);
 
 
 
 
 
 
159
  }
160
  .verdict-real {
161
  background: linear-gradient(135deg, #10b981, #059669);
162
  }
163
- .verdict-uncertain {
 
 
 
164
  background: linear-gradient(135deg, #f59e0b, #d97706);
165
  }
166
- .verdict-text {
167
- font-size: 2.2rem;
168
- font-weight: 800;
169
- letter-spacing: 0.12em;
170
- color: #ffffff;
171
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
 
174
- .glass-card {
175
- background: #ffffff;
176
- border: 1px solid #e5e9f2;
177
- border-radius: 18px;
178
- padding: 1.75rem;
179
- box-shadow: 0 8px 24px rgba(99, 102, 241, 0.07);
180
  }
181
 
182
- .footer-enhanced {
183
- margin-top: 2.5rem;
184
- padding: 1.5rem 2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  background: #ffffff;
186
  border: 1px solid #e5e9f2;
187
  border-radius: 16px;
188
- text-align: center;
189
- color: #64748b;
190
- box-shadow: 0 6px 18px rgba(99, 102, 241, 0.06);
191
  }
192
- .footer-features {
193
- display: flex;
194
- gap: 1.25rem;
195
  justify-content: center;
196
- flex-wrap: wrap;
 
 
 
 
 
197
  margin-bottom: 0.75rem;
198
  }
199
- .footer-feature {
200
- display: flex;
201
- align-items: center;
202
- gap: 0.4rem;
203
- font-size: 0.9rem;
204
- font-weight: 600;
205
- color: #475569;
206
  }
207
- .footer-feature-icon {
208
- font-size: 1.1rem;
 
 
 
 
 
 
 
 
 
 
209
  }
210
 
211
  .stButton > button, .stDownloadButton > button {
@@ -231,10 +309,9 @@ h1, h2, h3, h4 {
231
  .stButton > button[kind="secondary"]:hover {
232
  border-color: #6366f1;
233
  background: #f5f6ff;
234
- transform: translateY(-2px);
235
  }
236
  .stButton > button:disabled, .stDownloadButton > button:disabled {
237
- opacity: 0.55;
238
  box-shadow: none;
239
  }
240
 
@@ -294,10 +371,6 @@ h1, h2, h3, h4 {
294
  box-shadow: 0 6px 16px rgba(99, 102, 241, 0.06);
295
  }
296
 
297
- .stProgress > div > div > div {
298
- background: linear-gradient(90deg, #4f46e5, #7c3aed);
299
- }
300
-
301
  a {
302
  color: #6366f1;
303
  }
@@ -306,625 +379,563 @@ a {
306
 
307
 
308
  @st.cache_resource(show_spinner=False)
309
- def load_ai_models():
310
  try:
311
- with st.status("🔧 Loading AI models...", expanded=True) as status:
312
- st.write("🧠 Initializing Brain 1 (LIAR Political)...")
313
- classifier_b1 = pipeline(
314
  "text-classification",
315
- model=BRAIN_1_MODEL,
316
- tokenizer=BRAIN_1_MODEL,
317
  return_all_scores=False,
318
  device=0 if torch.cuda.is_available() else -1,
319
  )
320
- st.write("🎯 Initializing Brain 2 (General)...")
321
- classifier_b2 = pipeline(
322
  "text-classification",
323
- model=BRAIN_2_MODEL,
324
  return_all_scores=False,
325
  device=0 if torch.cuda.is_available() else -1,
326
  )
327
- status.update(label="✅ AI models loaded successfully!", state="complete")
328
- return classifier_b1, classifier_b2
329
  except Exception as e:
330
- st.error(f"🔴 Model loading failed: {str(e)}")
331
  return None, None
332
 
333
 
334
- def normalize_brain1_label(raw_label):
335
- label = str(raw_label).upper()
336
- if label in ("FAKE", "REAL"):
337
- return label
338
- if label == "LABEL_0":
339
- return "FAKE"
340
- if label == "LABEL_1":
341
- return "REAL"
342
- return label
343
-
344
-
345
- def normalize_brain2_label(raw_label):
346
- label = str(raw_label).upper()
347
- if label in ("FAKE", "REAL"):
348
- return label
349
- if label == "LABEL_1":
350
- return "FAKE"
351
- if label == "LABEL_0":
352
- return "REAL"
353
- return label
 
 
 
 
 
354
 
355
 
356
  def tavily_search(query):
357
  if not TAVILY_AVAILABLE:
358
- return None
359
  try:
360
- response = TAVILY_CLIENT.search(query, max_results=5)
361
- content_pieces = []
362
  for r in response.get("results", []):
363
- title = r.get("title", "")
364
- content = r.get("content", "")
365
- content_pieces.append(f"{title}: {content}")
366
- return "\n".join(content_pieces)
 
 
367
  except Exception:
 
 
 
 
 
368
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
 
371
- def is_us_political(text):
372
- keywords = [
373
- "president", "congress", "senate", "house", "democrat", "republican",
374
- "biden", "trump", "politics", "political", "us government", "white house",
375
- "politi", "liar", "election", "campaign", "supreme court",
376
- ]
377
- text_lower = text.lower()
378
- return any(kw in text_lower for kw in keywords)
 
 
 
 
 
 
 
 
379
 
380
 
381
- def generate_ai_explanation(text, classification, confidence):
382
  if not GROQ_AVAILABLE:
383
  return None
384
- prompt = (
385
- f"Analyze this content that an AI model classified as '{classification}' "
386
- f"with {confidence:.1f}% confidence.\n\n"
387
- f"Content: {text[:400]}...\n\n"
388
- f"Provide a concise professional explanation (2-4 sentences) of why this "
389
- f"classification is or isn't correct. If the classification appears wrong, "
390
- f"say so explicitly and explain."
391
- )
392
  for model in GROQ_MODELS:
393
  try:
394
  response = GROQ_CLIENT.chat.completions.create(
395
  model=model,
396
- messages=[{"role": "user", "content": prompt}],
397
- temperature=0.3,
398
- max_tokens=400,
 
 
 
 
399
  )
400
- content = response.choices[0].message.content
401
- if content and content.strip():
402
- return content.strip()
403
  except Exception:
404
  continue
405
  return None
406
 
407
 
408
- def analyze_with_models(text, classifier_b1, classifier_b2):
409
- text_stripped = text.strip()
410
- use_brain1 = is_us_political(text_stripped)
411
- brain_name = "Brain 1 (Political)" if use_brain1 else "Brain 2 (General)"
412
- classifier = classifier_b1 if use_brain1 else classifier_b2
413
-
414
- try:
415
- results = classifier(text_stripped, truncation=True)
416
- raw_label = results[0]["label"]
417
- if use_brain1:
418
- label = normalize_brain1_label(raw_label)
419
- else:
420
- label = normalize_brain2_label(raw_label)
421
- confidence = float(results[0]["score"]) * 100.0
422
- except Exception:
423
- label, confidence, summary = get_fallback_analysis(text_stripped)
424
- return label, confidence, summary, brain_name, False
425
-
426
- summary, corrected = build_summary(text_stripped, label, confidence)
427
- return corrected, confidence, summary, brain_name, True
428
-
429
-
430
- def build_summary(text, label, confidence):
431
- ai_explanation = generate_ai_explanation(text, label, confidence)
432
- if ai_explanation:
433
- lowered = ai_explanation.lower()
434
- markers = ["incorrect", "wrong", f"not {label.lower()}", "misclassification"]
435
- if any(marker in lowered for marker in markers):
436
- corrected = "REAL" if label == "FAKE" else "FAKE"
437
- return ai_explanation, corrected
438
- return ai_explanation, label
439
-
440
- if TAVILY_AVAILABLE:
441
- tavily_info = tavily_search(text)
442
- if tavily_info:
443
- return (
444
- f"Content classified as {label} by the model with {confidence:.1f}% "
445
- f"confidence, cross-referenced against live web sources.", label,
446
- )
447
- return (
448
- f"Content classified as {label} by the model with {confidence:.1f}% confidence.", label,
449
- )
450
 
451
 
452
- def get_fallback_analysis(text):
453
- fake_indicators = ['fake', 'hoax', 'conspiracy', 'false', 'lie', 'scam', 'fraud', 'misleading']
454
- real_indicators = ['study', 'research', 'according', 'official', 'confirmed', 'verified', 'report']
455
- text_lower = text.lower()
456
- fake_score = sum(1 for word in fake_indicators if word in text_lower)
457
- real_score = sum(1 for word in real_indicators if word in text_lower)
458
- if fake_score > real_score:
459
- return "FAKE", random.uniform(60.0, 80.0), "Fallback heuristic analysis: Likely FAKE content detected."
460
- if real_score > fake_score:
461
- return "REAL", random.uniform(60.0, 80.0), "Fallback heuristic analysis: Likely REAL content detected."
462
- return "UNCERTAIN", 50.0, "Fallback heuristic analysis: Unable to classify definitively."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
 
465
  @st.cache_data(show_spinner=False, ttl=300)
466
  def fetch_web_content(url):
467
  try:
468
  headers = {
469
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
470
- 'Chrome/124.0.0.0 Safari/537.36'
471
  }
472
  response = requests.get(url, headers=headers, timeout=15)
473
  response.raise_for_status()
474
- soup = BeautifulSoup(response.content, 'html.parser')
475
 
476
- for element in soup(['script', 'style', 'nav', 'footer', 'aside']):
477
  element.decompose()
478
 
479
- title_tag = soup.find('title')
480
  title = title_tag.get_text(strip=True) if title_tag else "No title found"
481
 
482
- paragraphs = soup.find_all('p')
483
  content = " ".join(
484
- p.get_text(strip=True)
485
- for p in paragraphs
486
- if len(p.get_text(strip=True)) > 20
487
  )
488
-
489
  full_text = f"{title}\n\n{content}"
490
  return {
491
- 'success': True,
492
- 'title': title,
493
- 'content': content,
494
- 'full_text': full_text,
495
- 'word_count': len(full_text.split()),
496
- 'url': url,
497
  }
498
  except Exception as e:
499
- return {'success': False, 'error': str(e)}
500
 
501
 
502
- def process_analysis(user_input, input_method, classifier_b1, classifier_b2):
503
  start_time = time.time()
504
- with st.status("🧠 Analyzing with dual-AI system...", expanded=True) as status:
505
- if input_method == "URL/Website" and user_input.startswith(('http://', 'https://')):
506
- st.write("🌐 Fetching content from URL...")
507
  web_data = fetch_web_content(user_input)
508
- if web_data['success']:
509
- text_to_analyze = web_data['full_text']
510
- st.write(f"✅ Successfully extracted {web_data['word_count']} words")
511
  else:
512
- st.error(f"❌ Failed to fetch content: {web_data['error']}")
513
  return
514
  else:
515
  text_to_analyze = user_input
516
 
517
- if len(text_to_analyze) > 3000:
518
- text_to_analyze = text_to_analyze[:3000]
519
- st.write("✂️ Text truncated for optimal processing")
520
 
521
- label, confidence, summary, brain_name, model_used = analyze_with_models(
522
- text_to_analyze, classifier_b1, classifier_b2
523
- )
524
 
525
  analysis_time = time.time() - start_time
526
- status.update(label="✅ Analysis complete!", state="complete")
527
 
528
  results = {
529
- 'verdict': label,
530
- 'confidence': confidence,
531
- 'summary': summary,
532
- 'analysis_time': analysis_time,
533
- 'brain': brain_name,
534
- 'model_used': model_used,
535
- 'input': user_input[:200] + "..." if len(user_input) > 200 else user_input,
536
- 'full_input': user_input,
537
  }
538
 
539
  st.session_state.current_results = results
540
  st.session_state.analysis_complete = True
541
 
542
- if 'analysis_history' not in st.session_state:
543
  st.session_state.analysis_history = []
544
  st.session_state.analysis_history.insert(0, results)
545
- if len(st.session_state.analysis_history) > 10:
546
- st.session_state.analysis_history = st.session_state.analysis_history[:10]
547
 
548
  st.rerun()
549
 
550
 
551
- def render_analysis_interface(classifier_b1, classifier_b2):
552
- st.markdown("### 🔍 Content Analysis")
553
- input_method = st.selectbox(
554
- "Select input method:",
555
- ["Direct Text", "URL/Website", "File Upload"],
556
- help="Choose how you want to provide content for fact-checking",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
  )
558
- user_input = ""
559
- if input_method == "Direct Text":
560
- user_input = st.text_area(
561
- "Enter text to analyze:",
562
- height=150,
563
- placeholder="Paste the content you want to fact-check here...",
564
- help="Enter any text content for misinformation detection",
565
- )
566
- elif input_method == "URL/Website":
567
- user_input = st.text_input(
568
- "Enter website URL:",
569
- placeholder="https://example.com/article",
570
- help="Provide the URL of an article or webpage to analyze",
571
  )
572
- if user_input and not user_input.startswith(('http://', 'https://')):
573
- st.warning("⚠️ Please enter a complete URL starting with http:// or https://")
574
- elif input_method == "File Upload":
575
- uploaded_file = st.file_uploader(
576
- "Upload text file:",
577
- type=['txt', 'md'],
578
- help="Upload a text file containing the content to analyze",
579
  )
580
- if uploaded_file:
581
- try:
582
- user_input = uploaded_file.getvalue().decode("utf-8", errors="replace")
583
- st.success(f"✅ File loaded: {len(user_input)} characters")
584
- if len(user_input) > 500:
585
- st.text_area(
586
- "Content preview:", user_input[:500] + "...", height=100, disabled=True
587
- )
588
- except Exception as e:
589
- st.error(f"❌ Error reading file: {str(e)}")
590
- user_input = ""
591
 
592
- st.markdown("---")
593
- col1, col2, col3 = st.columns([3, 1, 1])
594
- with col1:
595
- analyze_btn = st.button(
596
- "🧠 Analyze with Dual-AI",
597
- type="primary",
598
- disabled=not user_input.strip(),
599
- help="Start the AI-powered fact-checking analysis",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  )
601
- with col2:
602
- if st.button("🔄 Clear", help="Clear current results and start over"):
603
- st.session_state.analysis_complete = False
604
- st.session_state.current_results = {}
605
- st.rerun()
606
- with col3:
607
- export_enabled = st.session_state.get('analysis_complete', False)
608
- if st.button("📄 Export", disabled=not export_enabled, help="Export analysis results"):
609
- if export_enabled:
610
- export_results()
611
 
612
- if analyze_btn:
613
- if not user_input.strip():
614
- st.warning("⚠️ Please provide some content to analyze.")
615
- elif len(user_input.strip()) < 10:
616
- st.warning("⚠️ Please provide more content for meaningful analysis (minimum 10 characters).")
617
- elif input_method == "URL/Website" and not user_input.startswith(('http://', 'https://')):
618
- st.warning("⚠️ Please enter a valid URL starting with http:// or https://")
619
- else:
620
- process_analysis(user_input, input_method, classifier_b1, classifier_b2)
621
 
622
 
623
  def export_results():
624
- if not st.session_state.get('current_results'):
625
- st.warning("⚠️ No results to export!")
626
- return
627
  results = st.session_state.current_results
628
  export_data = {
629
- 'analysis_timestamp': datetime.now().isoformat(),
630
- 'input_text': results.get('full_input', results.get('input', '')),
631
- 'verdict': results.get('verdict', ''),
632
- 'confidence_score': float(results.get('confidence', 0)),
633
- 'ai_summary': results.get('summary', ''),
634
- 'analysis_model': results.get('brain', ''),
635
- 'analysis_time': results.get('analysis_time', 0),
636
  }
637
  json_string = json.dumps(export_data, indent=2, default=str, ensure_ascii=False)
638
  st.download_button(
639
- label="📥 Download Analysis Report",
640
  data=json_string,
641
- file_name=f"credo_ai_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
642
  mime="application/json",
643
  )
644
- st.success("📄 Analysis report ready for download!")
645
 
646
 
647
- def render_analysis_results(results):
648
- st.markdown("### ✨ AI-Powered Analysis Summary")
649
- st.markdown(
650
- f"""<div class="summary-box">{results['summary']}</div>""",
651
- unsafe_allow_html=True,
652
  )
653
- col1, col2 = st.columns(2, gap="large")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  with col1:
655
- st.markdown("### 🎯 Primary Verdict")
656
- verdict = results['verdict']
657
- confidence = results['confidence']
658
- verdict_class = 'verdict-real'
659
- if verdict == 'FAKE':
660
- verdict_class = 'verdict-fake'
661
- elif verdict == 'UNCERTAIN':
662
- verdict_class = 'verdict-uncertain'
663
- st.markdown(
664
- f"""
665
- <div class="verdict-container {verdict_class}">
666
- <div class="verdict-text">{verdict}</div>
667
- </div>
668
- <div style="text-align: center; margin-top: 1rem; font-size: 1.4rem; font-weight: 700; color: #334155;">
669
- {confidence:.1f}% Confidence
670
- </div>
671
- """,
672
- unsafe_allow_html=True,
673
  )
674
- st.progress(min(float(confidence) / 100.0, 1.0), text="Model confidence")
675
  with col2:
676
- st.markdown("### 📊 Analysis Details")
677
- st.metric("Processing Time", f"{results.get('analysis_time', 0):.2f}s")
678
- st.metric("Content Length", f"{len(results.get('input', '').split())} words")
679
- st.metric("Analysis Model", results.get('brain', 'Brain 2 (General)'))
680
- if results.get('model_used'):
681
- st.metric("Summary Layer", "Groq AI")
 
 
 
 
 
 
682
  else:
683
- st.metric("Summary Layer", "Built-in fallback")
684
 
685
 
686
- if 'analysis_complete' not in st.session_state:
687
  st.session_state.analysis_complete = False
688
- if 'current_results' not in st.session_state:
689
  st.session_state.current_results = {}
690
- if 'analysis_history' not in st.session_state:
691
  st.session_state.analysis_history = []
692
 
693
  with st.sidebar:
694
  st.markdown(
695
  """
696
- <div style="text-align: center; padding: 1rem 0; margin-bottom: 2rem;">
697
- <div style="font-size: 2.4rem; margin-bottom: 0.4rem;">🧠</div>
698
- <h2 style="color: #4f46e5; margin: 0;">Credo AI</h2>
699
- <p style="color: #94a3b8; margin: 0.5rem 0 0 0; font-size: 0.9rem;">Truth Detection Platform</p>
700
  </div>
701
  """,
702
  unsafe_allow_html=True,
703
  )
704
-
705
- page = st.radio(
706
- "Navigate:",
707
- ["🚀 Live Analysis", "📜 History", "ℹ️ About"],
708
- key="navigation",
709
- )
710
-
711
- if st.session_state.analysis_history:
712
- st.markdown("---")
713
- st.markdown("### 📈 Quick Stats")
714
- total = len(st.session_state.analysis_history)
715
- fake_count = sum(
716
- 1 for h in st.session_state.analysis_history if h.get('verdict') == 'FAKE'
717
- )
718
- st.metric("Total Analyses", total)
719
- if total > 0:
720
- st.metric("Fake Rate", f"{(fake_count / total * 100):.0f}%")
721
-
722
  st.markdown("---")
723
- st.markdown("### 🔧 Status")
724
- if GROQ_AVAILABLE:
725
- st.success("🟢 Groq Enhanced")
726
- else:
727
- st.warning("🟡 Basic Mode")
728
-
729
- st.markdown("---")
730
- if st.button("🗑️ Clear History", help="Clear all analysis history"):
731
  st.session_state.analysis_history = []
732
  st.session_state.analysis_complete = False
733
  st.session_state.current_results = {}
734
- st.success("History cleared!")
735
  time.sleep(1)
736
  st.rerun()
737
-
738
- if page == "🚀 Live Analysis":
739
  st.markdown(
740
  """
741
- <div class="hero-container">
742
- <h1 class="main-title">🧠 Credo AI Platform</h1>
743
- <p class="hero-subtitle">
744
- Next-generation misinformation detection powered by
745
- <strong>dual-AI architecture</strong>. Analyze text, articles, and claims
746
- with speed and insight.
747
- </p>
748
- <div class="metrics-container">
749
- <div class="metric-card">
750
- <span class="metric-value">2</span>
751
- <span class="metric-label">AI Brains</span>
752
- </div>
753
- <div class="metric-card">
754
- <span class="metric-value">FAKE/REAL</span>
755
- <span class="metric-label">Verdict</span>
756
- </div>
757
- <div class="metric-card">
758
- <span class="metric-value">&lt;3s</span>
759
- <span class="metric-label">Analysis Time</span>
760
- </div>
761
- </div>
762
  </div>
763
  """,
764
  unsafe_allow_html=True,
765
  )
766
 
767
- if not GROQ_AVAILABLE:
768
- st.info(
769
- "🔑 **Optional Setup:** Add GROQ_API_KEY in Space Settings → Variables and "
770
- "Secrets for AI-powered summaries with Groq. The platform works without it "
771
- "using intelligent fallback analysis."
772
- )
 
 
 
 
 
773
 
774
- classifier_b1, classifier_b2 = load_ai_models()
775
- if classifier_b1 is None or classifier_b2 is None:
776
- st.error("Failed to load AI models! Please restart the app or check logs.")
777
  else:
778
- render_analysis_interface(classifier_b1, classifier_b2)
779
-
780
- if st.session_state.analysis_complete and st.session_state.current_results:
781
- st.markdown("---")
782
- st.markdown("## 📊 Analysis Results")
783
- render_analysis_results(st.session_state.current_results)
784
-
785
- elif page == "📜 History":
786
- st.markdown("# 📜 Analysis History")
 
 
 
 
 
 
 
 
787
  if st.session_state.analysis_history:
 
 
788
  total = len(st.session_state.analysis_history)
789
- fake_count = sum(
790
- 1 for h in st.session_state.analysis_history if h.get('verdict') == 'FAKE'
791
- )
792
- real_count = sum(
793
- 1 for h in st.session_state.analysis_history if h.get('verdict') == 'REAL'
794
- )
795
- st.markdown("### 📈 Summary Statistics")
796
- stat_cols = st.columns(3)
797
- with stat_cols[0]:
798
- st.metric("Total Analyses", total)
799
- with stat_cols[1]:
800
- st.metric("Fake Content", fake_count)
801
- with stat_cols[2]:
802
- st.metric("Real Content", real_count)
803
  st.markdown("---")
804
  for i, result in enumerate(st.session_state.analysis_history):
805
- with st.expander(
806
- f"#{i + 1} - {result.get('verdict', 'Unknown')} | {result.get('input', 'No input')}",
807
- expanded=(i == 0),
808
- ):
809
- render_analysis_results(result)
 
 
810
  else:
811
- st.info(
812
- "📚 **No Analysis History** - Your analysis history will appear here after you "
813
- "perform some fact-checking analyses. Start by going to the Live Analysis page "
814
- "and analyzing some content!"
815
- )
816
 
817
- elif page == "ℹ️ About":
818
- st.markdown("# 🔬 About Credo AI")
819
  st.markdown(
820
  """
821
- <div class="glass-card">
822
- <h2 style="color: #4f46e5; margin-bottom: 1rem;">🚀 Revolutionary Detection Technology</h2>
823
- <p style="font-size: 1.2rem; color: #475569; line-height: 1.7;">
824
- Credo AI represents a breakthrough in automated fact-checking, combining
825
- <strong>two specialized neural networks</strong> with advanced language
826
- understanding to deliver rapid, transparent misinformation detection.
827
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
828
  </div>
829
  """,
830
  unsafe_allow_html=True,
831
  )
832
- tab1, tab2, tab3 = st.tabs(["🧠 AI Architecture", "📊 Performance", "🔬 Technology"])
833
-
834
- with tab1:
835
- st.markdown(
836
- """
837
- ### ⚡ Brain 2: The Specialist
838
- - **Model:** `Arko007/fact-check1-v3-final` (DeBERTa-v3-large)
839
- - **Function:** Rapid FAKE/REAL binary classification
840
- - **Training:** 50,000+ verified news articles + calibration passes
841
- - **Speed:** Sub-second inference time
842
-
843
- ### 🧠 Brain 1: The Political Expert
844
- - **Model:** `Arko007/fake-news-liar-political` (RoBERTa-base)
845
- - **Function:** Binary political fact-checking (US-centric)
846
- - **Training:** LIAR dataset converted to binary
847
- - **Performance:** ~71% accuracy
848
- - **Specialization:** Short political statement classification
849
-
850
- ### ⚡ Groq Integration
851
- - **Role:** Intelligent synthesis & explanation layer
852
- - **Model:** `qwen/qwen3.6-27b` (fallback: `openai/gpt-oss-120b`)
853
- - **Function:** Validates classifications and explains verdicts in plain language
854
- - **Speed:** Blazing-fast inference on Groq LPU hardware
855
- """
856
- )
857
-
858
- with tab2:
859
- st.markdown("### 📈 Performance Metrics")
860
- import pandas as pd
861
-
862
- metrics_data = {
863
- 'Metric': ['Accuracy', 'Precision', 'Recall', 'F1-Score', 'Speed'],
864
- 'Brain 1': ['71.4%', 'N/A', 'N/A', 'N/A', 'N/A'],
865
- 'Brain 2': ['99.9%', '99.8%', '99.7%', '99.7%', '0.8s'],
866
- 'Combined': ['~95%', 'N/A', 'N/A', 'N/A', '<3s'],
867
- }
868
- st.dataframe(
869
- pd.DataFrame(metrics_data),
870
- width="stretch",
871
- hide_index=True,
872
- )
873
- st.success("🏆 Credo AI blends specialized models to maximize coverage and accuracy.")
874
-
875
- with tab3:
876
- st.markdown(
877
- """
878
- ### 🛠️ Technology Stack
879
-
880
- **🤖 Core AI/ML:**
881
- - PyTorch deep learning framework
882
- - Hugging Face Transformers for model handling
883
- - RoBERTa & DeBERTa-v3 fine-tuned classifiers
884
-
885
- **🌐 Web & Integration:**
886
- - Streamlit for responsive UI
887
- - Beautiful Soup for web scraping
888
- - Groq Cloud API (`qwen/qwen3.6-27b`)
889
- - Tavily real-time information search
890
- - Custom CSS for enhanced UX
891
-
892
- **⚡ Performance:**
893
- - Intelligent caching system
894
- - Memory-efficient processing
895
- - Mobile-responsive design
896
- - Privacy-first architecture
897
- """
898
- )
899
 
900
  st.markdown(
901
- """
902
- <div class="footer-enhanced">
903
- <div class="footer-features">
904
- <div class="footer-feature">
905
- <div class="footer-feature-icon">🏆</div>
906
- <div class="footer-feature-text">Award Winning</div>
907
- </div>
908
- <div class="footer-feature">
909
- <div class="footer-feature-icon">⚡</div>
910
- <div class="footer-feature-text">Lightning Fast</div>
911
- </div>
912
- <div class="footer-feature">
913
- <div class="footer-feature-icon">🔒</div>
914
- <div class="footer-feature-text">Privacy First</div>
915
- </div>
916
- <div class="footer-feature">
917
- <div class="footer-feature-icon">🌍</div>
918
- <div class="footer-feature-text">Global Impact</div>
919
- </div>
920
- </div>
921
- <div style="font-size: 0.9rem; opacity: 0.85;">
922
- Built with ❤️ for Hack2Skill Hackathon 2025 | 🐉 Data Dragons Team
923
- </div>
924
- <div style="font-size: 0.8rem; opacity: 0.6; margin-top: 0.5rem;">
925
- Powered by Advanced AI • Making Truth Accessible to Everyone
926
- </div>
927
- </div>
928
- """,
929
  unsafe_allow_html=True,
930
  )
 
1
  import os
 
 
2
  import json
3
+ import time
4
  import requests
5
  from datetime import datetime
6
  from bs4 import BeautifulSoup
 
25
  TAVILY_CLIENT = None
26
  TAVILY_AVAILABLE = False
27
 
28
+ MODEL_POLITICAL = "Arko007/fake-news-liar-political"
29
+ MODEL_GENERAL = "Arko007/fact-check1-v3-final"
30
 
31
  GROQ_MODELS = [
32
  "qwen/qwen3.6-27b",
 
34
  "openai/gpt-oss-20b",
35
  ]
36
 
37
+ MAX_ANALYSIS_CHARS = 2000
38
+
39
  st.set_page_config(
40
+ page_title="Credo | Fact Check",
41
  page_icon="🧠",
42
  layout="wide",
43
  initial_sidebar_state="expanded",
 
45
 
46
  st.markdown("""
47
  <style>
 
 
48
  html, body, [class*="css"], [class*="st-"], .stApp {
49
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
50
+ "Helvetica Neue", Arial, "Noto Sans", sans-serif;
51
  }
52
 
53
  .stApp {
54
+ background: linear-gradient(135deg, #f6f8fc 0%, #eef2f9 55%, #eaf0fa 100%);
55
  color: #1e293b;
56
  }
57
 
58
  h1, h2, h3, h4 {
59
+ color: #0f172a;
60
  letter-spacing: -0.01em;
61
  }
62
 
 
68
  color: #475569;
69
  }
70
 
71
+ .brand {
 
 
 
 
72
  text-align: center;
73
+ padding: 1.25rem 0 0.5rem 0;
74
+ }
75
+ .brand-logo {
76
+ font-size: 2.2rem;
77
+ line-height: 1;
78
  }
79
+ .brand-name {
80
+ font-size: 1.4rem;
81
+ font-weight: 800;
82
+ margin: 0.4rem 0 0.1rem 0;
83
+ background: linear-gradient(90deg, #4f46e5, #7c3aed);
84
  -webkit-background-clip: text;
85
  -webkit-text-fill-color: transparent;
86
  background-clip: text;
87
+ }
88
+ .brand-tag {
89
+ color: #94a3b8;
90
+ font-size: 0.85rem;
91
+ margin: 0;
92
+ }
93
+
94
+ .hero {
95
+ text-align: center;
96
+ padding: 2.5rem 1rem 1.5rem 1rem;
97
+ max-width: 860px;
98
+ margin: 0 auto;
99
+ }
100
+ .hero-title {
101
+ font-size: 2.5rem;
102
  font-weight: 800;
103
+ color: #0f172a;
104
+ margin: 0 0 0.6rem 0;
105
  letter-spacing: -0.02em;
 
106
  }
107
+ .hero-title span {
108
+ background: linear-gradient(90deg, #4f46e5, #7c3aed);
109
+ -webkit-background-clip: text;
110
+ -webkit-text-fill-color: transparent;
111
+ background-clip: text;
112
  }
113
+ .hero-sub {
114
  color: #64748b;
115
  font-size: 1.1rem;
116
+ margin: 0 0 1.75rem 0;
 
 
117
  }
118
+
119
+ .trust-row {
120
  display: flex;
121
+ gap: 1.5rem;
122
  justify-content: center;
123
  flex-wrap: wrap;
124
+ margin-bottom: 2rem;
125
  }
126
+ .trust-chip {
127
  background: #ffffff;
128
  border: 1px solid #e5e9f2;
129
+ border-radius: 999px;
130
+ padding: 0.4rem 1.1rem;
131
+ color: #475569;
132
+ font-size: 0.85rem;
133
+ font-weight: 600;
134
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.05);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  }
136
 
137
+ .result-card {
138
  background: #ffffff;
139
  border: 1px solid #e5e9f2;
140
+ border-radius: 18px;
141
+ padding: 1.75rem 2rem;
142
+ box-shadow: 0 10px 30px rgba(99, 102, 241, 0.08);
143
+ margin-bottom: 1.25rem;
 
 
 
144
  }
145
 
146
+ .verdict-head {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 1.25rem;
150
+ flex-wrap: wrap;
151
+ margin-bottom: 1.25rem;
152
  }
153
+ .verdict-chip {
154
+ border-radius: 999px;
155
+ padding: 0.65rem 1.6rem;
156
+ font-size: 1.25rem;
157
+ font-weight: 800;
158
+ letter-spacing: 0.04em;
159
+ color: #ffffff;
160
+ box-shadow: 0 8px 20px rgba(15, 23, 42, 0.15);
161
  }
162
  .verdict-real {
163
  background: linear-gradient(135deg, #10b981, #059669);
164
  }
165
+ .verdict-fake {
166
+ background: linear-gradient(135deg, #ef4444, #dc2626);
167
+ }
168
+ .verdict-unknown {
169
  background: linear-gradient(135deg, #f59e0b, #d97706);
170
  }
171
+ .conf-line {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 0.75rem;
175
+ color: #64748b;
176
+ font-size: 0.95rem;
177
+ font-weight: 600;
178
+ }
179
+ .conf-bar {
180
+ width: 150px;
181
+ height: 8px;
182
+ border-radius: 999px;
183
+ background: #eef1f7;
184
+ overflow: hidden;
185
+ }
186
+ .conf-fill {
187
+ height: 100%;
188
+ border-radius: 999px;
189
+ background: linear-gradient(90deg, #4f46e5, #7c3aed);
190
  }
191
 
192
+ .explanation {
193
+ color: #334155;
194
+ font-size: 1.05rem;
195
+ line-height: 1.75;
196
+ margin: 0 0 1.5rem 0;
 
197
  }
198
 
199
+ .sub-block {
200
+ margin-bottom: 1.25rem;
201
+ }
202
+ .sub-title {
203
+ font-size: 0.8rem;
204
+ font-weight: 700;
205
+ text-transform: uppercase;
206
+ letter-spacing: 0.08em;
207
+ color: #94a3b8;
208
+ margin: 0 0 0.6rem 0;
209
+ }
210
+ .key-point {
211
+ display: flex;
212
+ gap: 0.6rem;
213
+ align-items: flex-start;
214
+ color: #334155;
215
+ padding: 0.45rem 0;
216
+ border-bottom: 1px dashed #eef1f7;
217
+ font-size: 0.98rem;
218
+ }
219
+ .key-point:last-child {
220
+ border-bottom: none;
221
+ }
222
+ .key-point .dot {
223
+ color: #6366f1;
224
+ font-weight: 800;
225
+ line-height: 1.4;
226
+ }
227
+ .source-link {
228
+ display: block;
229
+ color: #4f46e5;
230
+ text-decoration: none;
231
+ font-weight: 600;
232
+ font-size: 0.95rem;
233
+ padding: 0.4rem 0;
234
+ border-bottom: 1px dashed #eef1f7;
235
+ }
236
+ .source-link:last-child {
237
+ border-bottom: none;
238
+ }
239
+ .source-link:hover {
240
+ color: #7c3aed;
241
+ }
242
+
243
+ .steps-grid {
244
+ display: flex;
245
+ gap: 1.25rem;
246
+ flex-wrap: wrap;
247
+ margin: 1.5rem 0;
248
+ }
249
+ .step-card {
250
+ flex: 1;
251
+ min-width: 220px;
252
  background: #ffffff;
253
  border: 1px solid #e5e9f2;
254
  border-radius: 16px;
255
+ padding: 1.5rem;
256
+ box-shadow: 0 8px 22px rgba(99, 102, 241, 0.06);
 
257
  }
258
+ .step-num {
259
+ display: inline-flex;
260
+ align-items: center;
261
  justify-content: center;
262
+ width: 34px;
263
+ height: 34px;
264
+ border-radius: 10px;
265
+ background: linear-gradient(135deg, #4f46e5, #7c3aed);
266
+ color: #ffffff;
267
+ font-weight: 800;
268
  margin-bottom: 0.75rem;
269
  }
270
+ .step-title {
271
+ font-weight: 700;
272
+ color: #0f172a;
273
+ margin: 0 0 0.4rem 0;
 
 
 
274
  }
275
+ .step-text {
276
+ color: #64748b;
277
+ font-size: 0.95rem;
278
+ line-height: 1.6;
279
+ margin: 0;
280
+ }
281
+
282
+ .footer {
283
+ text-align: center;
284
+ color: #94a3b8;
285
+ font-size: 0.85rem;
286
+ padding: 2rem 1rem 1rem 1rem;
287
  }
288
 
289
  .stButton > button, .stDownloadButton > button {
 
309
  .stButton > button[kind="secondary"]:hover {
310
  border-color: #6366f1;
311
  background: #f5f6ff;
 
312
  }
313
  .stButton > button:disabled, .stDownloadButton > button:disabled {
314
+ opacity: 0.5;
315
  box-shadow: none;
316
  }
317
 
 
371
  box-shadow: 0 6px 16px rgba(99, 102, 241, 0.06);
372
  }
373
 
 
 
 
 
374
  a {
375
  color: #6366f1;
376
  }
 
379
 
380
 
381
  @st.cache_resource(show_spinner=False)
382
+ def load_engines():
383
  try:
384
+ with st.status("🔧 Setting up analysis engines...", expanded=True) as status:
385
+ st.write("📚 Loading the fact-checking models...")
386
+ clf_political = pipeline(
387
  "text-classification",
388
+ model=MODEL_POLITICAL,
389
+ tokenizer=MODEL_POLITICAL,
390
  return_all_scores=False,
391
  device=0 if torch.cuda.is_available() else -1,
392
  )
393
+ st.write(" Preparing the general model...")
394
+ clf_general = pipeline(
395
  "text-classification",
396
+ model=MODEL_GENERAL,
397
  return_all_scores=False,
398
  device=0 if torch.cuda.is_available() else -1,
399
  )
400
+ status.update(label="✅ Analysis engines ready!", state="complete")
401
+ return clf_political, clf_general
402
  except Exception as e:
403
+ st.error(f"🔴 Setup failed: {str(e)}")
404
  return None, None
405
 
406
 
407
+ def classify_with_models(text, clf_political, clf_general):
408
+ results = {"political": None, "general": None}
409
+ if clf_political is not None:
410
+ try:
411
+ raw = clf_political(text, truncation=True)[0]
412
+ label = str(raw["label"]).upper()
413
+ if label == "LABEL_0":
414
+ label = "FAKE"
415
+ elif label == "LABEL_1":
416
+ label = "REAL"
417
+ results["political"] = {"verdict": label, "score": float(raw["score"])}
418
+ except Exception:
419
+ results["political"] = None
420
+ if clf_general is not None:
421
+ try:
422
+ raw = clf_general(text, truncation=True)[0]
423
+ label = str(raw["label"]).upper()
424
+ if label == "LABEL_1":
425
+ label = "FAKE"
426
+ elif label == "LABEL_0":
427
+ label = "REAL"
428
+ results["general"] = {"verdict": label, "score": float(raw["score"])}
429
+ except Exception:
430
+ results["general"] = None
431
+ return results
432
 
433
 
434
  def tavily_search(query):
435
  if not TAVILY_AVAILABLE:
436
+ return []
437
  try:
438
+ response = TAVILY_CLIENT.search(query[:400], max_results=5)
439
+ results = []
440
  for r in response.get("results", []):
441
+ results.append({
442
+ "title": r.get("title", ""),
443
+ "url": r.get("url", ""),
444
+ "content": r.get("content", ""),
445
+ })
446
+ return results
447
  except Exception:
448
+ return []
449
+
450
+
451
+ def parse_llm_json(raw):
452
+ if not raw:
453
  return None
454
+ text = raw.strip()
455
+ if text.startswith("```"):
456
+ text = text.strip("`")
457
+ if text.startswith("json"):
458
+ text = text[4:]
459
+ try:
460
+ return json.loads(text)
461
+ except Exception:
462
+ pass
463
+ start = text.find("{")
464
+ end = text.rfind("}")
465
+ if start != -1 and end > start:
466
+ try:
467
+ return json.loads(text[start:end + 1])
468
+ except Exception:
469
+ return None
470
+ return None
471
 
472
 
473
+ SYSTEM_PROMPT = (
474
+ "You are a professional fact-checking analyst working inside a consumer fact-checking "
475
+ "app. You will receive a piece of content, two AI classifier verdicts with confidence "
476
+ "scores, and web search evidence. Your job is to decide how truthful the content is. "
477
+ "You are the final judge: your verdict overrides the classifier verdicts.\n"
478
+ 'Respond ONLY with a valid JSON object in this exact shape:\n'
479
+ '{"verdict": "REAL" or "FAKE" or "UNKNOWN", "confidence": <integer 0-100>, '
480
+ '"explanation": "<2-3 plain-language sentences>", '
481
+ '"key_points": ["short point", "short point", "short point"], '
482
+ '"sources": ["source title", "source title"]}\n'
483
+ "Rules:\n"
484
+ "- Use the web evidence as the strongest signal; the classifiers are secondary.\n"
485
+ "- Choose UNKNOWN only when there is genuinely not enough evidence to decide.\n"
486
+ "- Never mention classifiers, models, AI, or internal details in the explanation.\n"
487
+ "- Keep the explanation and key points in simple language a general audience understands."
488
+ )
489
 
490
 
491
+ def synthesize_verdict(content, model_results, evidence):
492
  if not GROQ_AVAILABLE:
493
  return None
494
+ payload = {
495
+ "content": content,
496
+ "classifier_verdicts": {k: v for k, v in model_results.items() if v},
497
+ "web_evidence": [
498
+ {"title": s["title"], "content": s["content"][:200]} for s in evidence
499
+ ],
500
+ }
501
+ prompt = json.dumps(payload, ensure_ascii=False)[:6000]
502
  for model in GROQ_MODELS:
503
  try:
504
  response = GROQ_CLIENT.chat.completions.create(
505
  model=model,
506
+ messages=[
507
+ {"role": "system", "content": SYSTEM_PROMPT},
508
+ {"role": "user", "content": prompt},
509
+ ],
510
+ temperature=0.2,
511
+ max_tokens=700,
512
+ response_format={"type": "json_object"},
513
  )
514
+ data = parse_llm_json(response.choices[0].message.content)
515
+ if data and data.get("verdict") in ("REAL", "FAKE", "UNKNOWN"):
516
+ return data
517
  except Exception:
518
  continue
519
  return None
520
 
521
 
522
+ def fallback_verdict(model_results):
523
+ preferred = None
524
+ for key in ("general", "political"):
525
+ if model_results.get(key):
526
+ preferred = model_results[key]
527
+ break
528
+ if preferred is None:
529
+ return "UNKNOWN", 0, (
530
+ "We couldn't analyze this content right now. Please try again in a moment."
531
+ )
532
+ label = preferred["verdict"]
533
+ confidence = int(round(preferred["score"] * 100))
534
+ if label == "FAKE":
535
+ explanation = (
536
+ "This content contains patterns commonly associated with misleading or "
537
+ "false information. We recommend double-checking it against trusted sources."
538
+ )
539
+ elif label == "REAL":
540
+ explanation = (
541
+ "This content looks consistent with factual reporting. It matches patterns "
542
+ "commonly found in verified, reliable information."
543
+ )
544
+ else:
545
+ explanation = "We couldn't determine the truthfulness of this content with confidence."
546
+ return label, confidence, explanation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
 
548
 
549
+ def run_analysis(text, clf_political, clf_general):
550
+ content = text.strip()
551
+ if len(content) > MAX_ANALYSIS_CHARS:
552
+ content = content[:MAX_ANALYSIS_CHARS]
553
+ model_results = classify_with_models(content, clf_political, clf_general)
554
+ evidence = tavily_search(content)
555
+ llm_data = None
556
+ if GROQ_AVAILABLE:
557
+ llm_data = synthesize_verdict(content, model_results, evidence)
558
+ if llm_data:
559
+ verdict = llm_data.get("verdict", "UNKNOWN")
560
+ confidence = int(llm_data.get("confidence", 0))
561
+ explanation = llm_data.get("explanation", "")
562
+ key_points = llm_data.get("key_points", [])
563
+ sources = llm_data.get("sources", [])
564
+ else:
565
+ verdict, confidence, explanation = fallback_verdict(model_results)
566
+ key_points = []
567
+ sources = []
568
+ return {
569
+ "verdict": verdict,
570
+ "confidence": confidence,
571
+ "explanation": explanation,
572
+ "key_points": key_points,
573
+ "sources": sources,
574
+ "evidence": evidence,
575
+ }
576
 
577
 
578
  @st.cache_data(show_spinner=False, ttl=300)
579
  def fetch_web_content(url):
580
  try:
581
  headers = {
582
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
583
+ "Chrome/124.0.0.0 Safari/537.36"
584
  }
585
  response = requests.get(url, headers=headers, timeout=15)
586
  response.raise_for_status()
587
+ soup = BeautifulSoup(response.content, "html.parser")
588
 
589
+ for element in soup(["script", "style", "nav", "footer", "aside"]):
590
  element.decompose()
591
 
592
+ title_tag = soup.find("title")
593
  title = title_tag.get_text(strip=True) if title_tag else "No title found"
594
 
595
+ paragraphs = soup.find_all("p")
596
  content = " ".join(
597
+ p.get_text(strip=True) for p in paragraphs if len(p.get_text(strip=True)) > 20
 
 
598
  )
 
599
  full_text = f"{title}\n\n{content}"
600
  return {
601
+ "success": True,
602
+ "title": title,
603
+ "full_text": full_text,
604
+ "word_count": len(full_text.split()),
 
 
605
  }
606
  except Exception as e:
607
+ return {"success": False, "error": str(e)}
608
 
609
 
610
+ def process_analysis(user_input, input_method, clf_political, clf_general):
611
  start_time = time.time()
612
+ with st.status("🔍 Analyzing...", expanded=True) as status:
613
+ if input_method == "URL/Website" and user_input.startswith(("http://", "https://")):
614
+ st.write("🌐 Reading the article...")
615
  web_data = fetch_web_content(user_input)
616
+ if web_data["success"]:
617
+ text_to_analyze = web_data["full_text"]
618
+ st.write(f"✅ Got {web_data['word_count']} words from the page")
619
  else:
620
+ st.error(f"❌ Couldn't read that page: {web_data['error']}")
621
  return
622
  else:
623
  text_to_analyze = user_input
624
 
625
+ if len(text_to_analyze) > MAX_ANALYSIS_CHARS:
626
+ text_to_analyze = text_to_analyze[:MAX_ANALYSIS_CHARS]
627
+ st.write("✂️ Focused on the most relevant part of the content")
628
 
629
+ st.write("🌐 Checking claims across the web...")
630
+ result = run_analysis(text_to_analyze, clf_political, clf_general)
 
631
 
632
  analysis_time = time.time() - start_time
633
+ status.update(label="✅ Analysis ready!", state="complete")
634
 
635
  results = {
636
+ **result,
637
+ "analysis_time": analysis_time,
638
+ "input": user_input[:200] + "..." if len(user_input) > 200 else user_input,
639
+ "full_input": user_input,
640
+ "timestamp": datetime.now().strftime("%b %d, %Y at %H:%M"),
 
 
 
641
  }
642
 
643
  st.session_state.current_results = results
644
  st.session_state.analysis_complete = True
645
 
646
+ if "analysis_history" not in st.session_state:
647
  st.session_state.analysis_history = []
648
  st.session_state.analysis_history.insert(0, results)
649
+ if len(st.session_state.analysis_history) > 20:
650
+ st.session_state.analysis_history = st.session_state.analysis_history[:20]
651
 
652
  st.rerun()
653
 
654
 
655
+ def confidence_band(confidence):
656
+ if confidence >= 70:
657
+ return "High confidence"
658
+ if confidence >= 45:
659
+ return "Medium confidence"
660
+ return "Low confidence"
661
+
662
+
663
+ def render_result(results, compact=False):
664
+ verdict = results["verdict"]
665
+ confidence = int(results.get("confidence", 0))
666
+ band = confidence_band(confidence)
667
+
668
+ chip_text = {"REAL": "Real", "FAKE": "Fake", "UNKNOWN": "Unclear"}.get(verdict, "Unclear")
669
+ chip_class = {
670
+ "REAL": "verdict-real",
671
+ "FAKE": "verdict-fake",
672
+ "UNKNOWN": "verdict-unknown",
673
+ }.get(verdict, "verdict-unknown")
674
+
675
+ st.markdown(
676
+ f"""
677
+ <div class="result-card">
678
+ <div class="verdict-head">
679
+ <div class="verdict-chip {chip_class}">{chip_text}</div>
680
+ <div class="conf-line">
681
+ {band}
682
+ <div class="conf-bar">
683
+ <div class="conf-fill" style="width: {max(min(confidence, 100), 4)}%;"></div>
684
+ </div>
685
+ </div>
686
+ </div>
687
+ <p class="explanation">{results.get("explanation", "")}</p>
688
+ """,
689
+ unsafe_allow_html=True,
690
  )
691
+
692
+ key_points = results.get("key_points") or []
693
+ if key_points:
694
+ points_html = "".join(
695
+ f'<div class="key-point"><span class="dot">•</span><span>{p}</span></div>'
696
+ for p in key_points
 
 
 
 
 
 
 
697
  )
698
+ st.markdown(
699
+ f'<div class="sub-block"><div class="sub-title">Key takeaways</div>{points_html}</div>',
700
+ unsafe_allow_html=True,
 
 
 
 
701
  )
 
 
 
 
 
 
 
 
 
 
 
702
 
703
+ sources = results.get("sources") or []
704
+ evidence = results.get("evidence") or []
705
+ source_links = []
706
+ if sources:
707
+ source_links.extend(sources)
708
+ if not source_links:
709
+ for e in evidence[:4]:
710
+ if e.get("url"):
711
+ source_links.append(f'{e["title"] or e["url"]} ({e["url"]})')
712
+ if source_links:
713
+ links_html = ""
714
+ for src in source_links[:5]:
715
+ url = None
716
+ label = src
717
+ if "http" in src:
718
+ end = src.rfind(" (http")
719
+ if end != -1:
720
+ label = src[:end]
721
+ url = src[end + 2:-1]
722
+ if url:
723
+ links_html += f'<a class="source-link" href="{url}" target="_blank">{label}</a>'
724
+ else:
725
+ links_html += f'<div class="source-link">{label}</div>'
726
+ st.markdown(
727
+ f'<div class="sub-block"><div class="sub-title">Sources</div>{links_html}</div>',
728
+ unsafe_allow_html=True,
729
  )
 
 
 
 
 
 
 
 
 
 
730
 
731
+ if results.get("timestamp"):
732
+ st.caption(f"Analyzed {results['timestamp']} · {results.get('analysis_time', 0):.1f}s")
733
+ st.markdown("</div>", unsafe_allow_html=True)
 
 
 
 
 
 
734
 
735
 
736
  def export_results():
 
 
 
737
  results = st.session_state.current_results
738
  export_data = {
739
+ "timestamp": datetime.now().isoformat(),
740
+ "input_text": results.get("full_input", results.get("input", "")),
741
+ "verdict": results.get("verdict", ""),
742
+ "confidence": int(results.get("confidence", 0)),
743
+ "explanation": results.get("explanation", ""),
744
+ "key_points": results.get("key_points", []),
745
+ "sources": results.get("sources", []),
746
  }
747
  json_string = json.dumps(export_data, indent=2, default=str, ensure_ascii=False)
748
  st.download_button(
749
+ label="📥 Download report",
750
  data=json_string,
751
+ file_name=f"credo_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
752
  mime="application/json",
753
  )
 
754
 
755
 
756
+ def render_input_interface(clf_political, clf_general):
757
+ input_method = st.selectbox(
758
+ "How would you like to provide the content?",
759
+ ["Paste text", "Article link", "Upload a file"],
 
760
  )
761
+ user_input = ""
762
+ if input_method == "Paste text":
763
+ user_input = st.text_area(
764
+ "Paste a claim, headline, or article excerpt",
765
+ height=160,
766
+ placeholder="Paste the text you want to fact-check...",
767
+ )
768
+ elif input_method == "Article link":
769
+ user_input = st.text_input(
770
+ "Article URL",
771
+ placeholder="https://example.com/article",
772
+ )
773
+ if user_input and not user_input.startswith(("http://", "https://")):
774
+ st.warning("Please enter a complete URL starting with http:// or https://")
775
+ else:
776
+ uploaded = st.file_uploader("Upload a text file", type=["txt", "md"])
777
+ if uploaded:
778
+ user_input = uploaded.getvalue().decode("utf-8", errors="replace")
779
+ st.success(f"Loaded {len(user_input)} characters")
780
+
781
+ col1, col2 = st.columns([3, 1])
782
  with col1:
783
+ analyze_btn = st.button(
784
+ "🧠 Fact-check now",
785
+ type="primary",
786
+ disabled=not user_input.strip(),
787
+ use_container_width=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
788
  )
 
789
  with col2:
790
+ if st.button("🗑️ Clear", disabled=not (user_input or st.session_state.analysis_complete)):
791
+ st.session_state.analysis_complete = False
792
+ st.session_state.current_results = {}
793
+ st.rerun()
794
+
795
+ if analyze_btn:
796
+ if not user_input.strip():
797
+ st.warning("Please add some content to fact-check.")
798
+ elif len(user_input.strip()) < 10:
799
+ st.warning("Please add a bit more text — at least 10 characters.")
800
+ elif input_method == "Article link" and not user_input.startswith(("http://", "https://")):
801
+ st.warning("Please enter a valid URL.")
802
  else:
803
+ process_analysis(user_input, input_method, clf_political, clf_general)
804
 
805
 
806
+ if "analysis_complete" not in st.session_state:
807
  st.session_state.analysis_complete = False
808
+ if "current_results" not in st.session_state:
809
  st.session_state.current_results = {}
810
+ if "analysis_history" not in st.session_state:
811
  st.session_state.analysis_history = []
812
 
813
  with st.sidebar:
814
  st.markdown(
815
  """
816
+ <div class="brand">
817
+ <div class="brand-logo">🧠</div>
818
+ <div class="brand-name">Credo</div>
819
+ <p class="brand-tag">Truth, verified.</p>
820
  </div>
821
  """,
822
  unsafe_allow_html=True,
823
  )
824
+ page = st.radio("Menu", ["✅ Fact Check", "🕘 History", "ℹ️ How it works"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  st.markdown("---")
826
+ if st.button("🗑️ Clear history"):
 
 
 
 
 
 
 
827
  st.session_state.analysis_history = []
828
  st.session_state.analysis_complete = False
829
  st.session_state.current_results = {}
830
+ st.success("History cleared")
831
  time.sleep(1)
832
  st.rerun()
 
 
833
  st.markdown(
834
  """
835
+ <div class="trust-row" style="flex-direction: column; gap: 0.5rem; margin-top: 1rem;">
836
+ <div class="trust-chip">Free to use</div>
837
+ <div class="trust-chip">Private — nothing is stored</div>
838
+ <div class="trust-chip">Results in seconds</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
839
  </div>
840
  """,
841
  unsafe_allow_html=True,
842
  )
843
 
844
+ if page == "✅ Fact Check":
845
+ st.markdown(
846
+ """
847
+ <div class="hero">
848
+ <h1 class="hero-title">Check any claim, <span>before you share it.</span></h1>
849
+ <p class="hero-sub">Paste a headline, an article, or a link — get a clear
850
+ verdict with sources in seconds.</p>
851
+ </div>
852
+ """,
853
+ unsafe_allow_html=True,
854
+ )
855
 
856
+ clf_political, clf_general = load_engines()
857
+ if clf_political is None and clf_general is None:
858
+ st.error("The analysis engines failed to start. Please reload the page to retry.")
859
  else:
860
+ render_input_interface(clf_political, clf_general)
861
+
862
+ if st.session_state.analysis_complete and st.session_state.current_results:
863
+ st.markdown("### Your result")
864
+ render_result(st.session_state.current_results)
865
+ st.markdown("---")
866
+ col_a, col_b = st.columns([1, 3])
867
+ with col_a:
868
+ export_results()
869
+ with col_b:
870
+ st.markdown(
871
+ "⚠️ AI-generated analysis — always verify important information with "
872
+ "trusted sources before sharing."
873
+ )
874
+
875
+ elif page == "🕘 History":
876
+ st.markdown("# 🕘 Your history")
877
  if st.session_state.analysis_history:
878
+ st.markdown("### At a glance")
879
+ stats = st.columns(3)
880
  total = len(st.session_state.analysis_history)
881
+ fake_count = sum(1 for h in st.session_state.analysis_history if h.get("verdict") == "FAKE")
882
+ real_count = sum(1 for h in st.session_state.analysis_history if h.get("verdict") == "REAL")
883
+ stats[0].metric("Checks", total)
884
+ stats[1].metric("Marked fake", fake_count)
885
+ stats[2].metric("Marked real", real_count)
 
 
 
 
 
 
 
 
 
886
  st.markdown("---")
887
  for i, result in enumerate(st.session_state.analysis_history):
888
+ chip = {
889
+ "REAL": "✅ Real",
890
+ "FAKE": "🚩 Fake",
891
+ "UNKNOWN": "⚠️ Unclear",
892
+ }.get(result.get("verdict"), "⚠️ Unclear")
893
+ with st.expander(f"{chip} · {result.get('input', '')}", expanded=(i == 0)):
894
+ render_result(result)
895
  else:
896
+ st.info("Nothing here yet. Run your first fact check and it will show up here.")
 
 
 
 
897
 
898
+ elif page == "ℹ️ How it works":
899
+ st.markdown("# ℹ️ How it works")
900
  st.markdown(
901
  """
902
+ <div class="steps-grid">
903
+ <div class="step-card">
904
+ <div class="step-num">1</div>
905
+ <p class="step-title">You share the content</p>
906
+ <p class="step-text">Paste a claim, headline, article, or a link to a page.</p>
907
+ </div>
908
+ <div class="step-card">
909
+ <div class="step-num">2</div>
910
+ <p class="step-title">We cross-check it</p>
911
+ <p class="step-text">Your content is checked against live web sources and
912
+ analyzed by AI trained to spot misleading patterns.</p>
913
+ </div>
914
+ <div class="step-card">
915
+ <div class="step-num">3</div>
916
+ <p class="step-title">You get a clear verdict</p>
917
+ <p class="step-text">A simple verdict — Real, Fake, or Unclear — with
918
+ confidence, key takeaways, and sources.</p>
919
+ </div>
920
+ </div>
921
+ """,
922
+ unsafe_allow_html=True,
923
+ )
924
+ st.markdown(
925
+ """
926
+ <div class="result-card">
927
+ <div class="sub-title">The honest part</div>
928
+ <p class="step-text">Credo is a helpful first filter, not a perfect truth machine.
929
+ AI pattern detection works best on news-style text and can miss false claims that
930
+ look completely normal. Always check important information with trusted sources
931
+ before you share it.</p>
932
  </div>
933
  """,
934
  unsafe_allow_html=True,
935
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
 
937
  st.markdown(
938
+ '<div class="footer">Made with ❤️ for the Hack2Skill Hackathon 2025 · '
939
+ "Credo — truth, verified.</div>",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
940
  unsafe_allow_html=True,
941
  )