Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import os | |
| import requests | |
| from typing import Dict, Optional | |
| import time | |
| from datetime import datetime | |
| # Page configuration | |
| st.set_page_config( | |
| page_title="VeriFact - Claim Verifier", | |
| page_icon="π", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # FIXED CSS - Much better colors and visibility! | |
| st.markdown(""" | |
| <style> | |
| /* Main background */ | |
| .main { | |
| background-color: #ffffff; | |
| } | |
| /* Headers with good contrast */ | |
| .main-header { | |
| font-size: 3rem; | |
| font-weight: bold; | |
| color: #1a1a1a; | |
| text-align: center; | |
| margin-bottom: 0.5rem; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .sub-header { | |
| text-align: center; | |
| color: #444444; | |
| margin-bottom: 2rem; | |
| font-size: 1.2rem; | |
| } | |
| /* TRUE verdict - Green with dark text */ | |
| .verdict-true { | |
| background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); | |
| border-left: 6px solid #28a745; | |
| padding: 1.5rem; | |
| border-radius: 8px; | |
| margin: 1rem 0; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| color: #155724 !important; | |
| } | |
| .verdict-true h3, .verdict-true p, .verdict-true strong { | |
| color: #155724 !important; | |
| } | |
| /* FALSE verdict - Red with dark text */ | |
| .verdict-false { | |
| background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%); | |
| border-left: 6px solid #dc3545; | |
| padding: 1.5rem; | |
| border-radius: 8px; | |
| margin: 1rem 0; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| color: #721c24 !important; | |
| } | |
| .verdict-false h3, .verdict-false p, .verdict-false strong { | |
| color: #721c24 !important; | |
| } | |
| /* MIXED verdict - Yellow with dark text */ | |
| .verdict-mixed { | |
| background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%); | |
| border-left: 6px solid #ffc107; | |
| padding: 1.5rem; | |
| border-radius: 8px; | |
| margin: 1rem 0; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| color: #856404 !important; | |
| } | |
| .verdict-mixed h3, .verdict-mixed p, .verdict-mixed strong { | |
| color: #856404 !important; | |
| } | |
| /* UNVERIFIED - Blue with dark text */ | |
| .verdict-unverified { | |
| background: linear-gradient(135deg, #e7f3ff 0%, #cfe2ff 100%); | |
| border-left: 6px solid #0d6efd; | |
| padding: 1.5rem; | |
| border-radius: 8px; | |
| margin: 1rem 0; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| color: #084298 !important; | |
| } | |
| .verdict-unverified h3, .verdict-unverified p, .verdict-unverified strong { | |
| color: #084298 !important; | |
| } | |
| /* Links */ | |
| .verdict-true a, .verdict-false a, .verdict-mixed a, .verdict-unverified a { | |
| color: #0066cc !important; | |
| text-decoration: underline; | |
| font-weight: bold; | |
| } | |
| /* Stats boxes */ | |
| .stat-box { | |
| background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); | |
| padding: 2rem 1rem; | |
| border-radius: 12px; | |
| text-align: center; | |
| margin: 0.5rem; | |
| border: 2px solid #dee2e6; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| } | |
| /* Fix input & textarea visibility */ | |
| textarea, input { | |
| background-color: #f8f9fa !important; | |
| color: #000000 !important; | |
| border: 2px solid #ced4da !important; | |
| border-radius: 8px !important; | |
| font-size: 16px !important; | |
| } | |
| textarea::placeholder, input::placeholder { | |
| color: #6c757d !important; | |
| } | |
| .stat-box h2 { | |
| margin: 0 !important; | |
| font-size: 2.5rem !important; | |
| } | |
| .stat-box p { | |
| color: #495057 !important; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| } | |
| /* Section titles (bold black headings) */ | |
| .section-title { | |
| font-size: 1.7rem; | |
| font-weight: 800; | |
| color: #000000 !important; /* force black */ | |
| margin-top: 2rem; | |
| margin-bottom: 1rem; | |
| } | |
| .section-title strong { | |
| color: #000000 !important; /* force black for strong text */ | |
| font-weight: 900; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ============================== | |
| # Google Fact Check API Class | |
| # ============================== | |
| class GoogleFactCheckAPI: | |
| """Google Fact Check Tools API Integration""" | |
| def __init__(self, api_key: Optional[str] = None): | |
| self.api_key = api_key or os.environ.get('GOOGLE_API_KEY') | |
| self.base_url = "https://factchecktools.googleapis.com/v1alpha1/claims:search" | |
| if not self.api_key: | |
| st.markdown(""" | |
| <div class="verdict-mixed"> | |
| <p>β οΈ Google API Key not found. Add it in Settings β Repository secrets</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| def verify_claim(self, claim: str, language: str = "en") -> Dict: | |
| if not self.api_key: | |
| return {"status": "error", "message": "β API key not configured. Please add GOOGLE_API_KEY in Hugging Face Secrets."} | |
| try: | |
| params = {"key": self.api_key, "query": claim, "languageCode": language, "pageSize": 10} | |
| response = requests.get(self.base_url, params=params, timeout=15) | |
| response.raise_for_status() | |
| data = response.json() | |
| if "claims" not in data or len(data["claims"]) == 0: | |
| simplified_query = self._simplify_claim(claim) | |
| if simplified_query != claim: | |
| params["query"] = simplified_query | |
| response = requests.get(self.base_url, params=params, timeout=15) | |
| response.raise_for_status() | |
| data = response.json() | |
| if "claims" not in data or len(data["claims"]) == 0: | |
| return {"status": "no_results", "message": f"No fact-checks found for: '{claim}'"} | |
| results = [] | |
| for claim_data in data["claims"]: | |
| for review in claim_data.get("claimReview", []): | |
| results.append({ | |
| "claim_text": claim_data.get("text", ""), | |
| "claimant": claim_data.get("claimant", "Unknown"), | |
| "claim_date": claim_data.get("claimDate", "Unknown"), | |
| "publisher": review.get("publisher", {}).get("name", "Unknown"), | |
| "url": review.get("url", ""), | |
| "title": review.get("title", ""), | |
| "rating": review.get("textualRating", "Unknown"), | |
| "language": review.get("languageCode", "en") | |
| }) | |
| return {"status": "success", "claim": claim, "results": results, "count": len(results)} | |
| except requests.exceptions.HTTPError as e: | |
| if e.response.status_code == 403: | |
| return {"status": "error", "message": "β API key is invalid or Fact Check API is not enabled. Check Google Cloud Console."} | |
| return {"status": "error", "message": f"β HTTP Error: {str(e)}"} | |
| except requests.exceptions.RequestException as e: | |
| return {"status": "error", "message": f"β Network error: {str(e)}"} | |
| except Exception as e: | |
| return {"status": "error", "message": f"β Unexpected error: {str(e)}"} | |
| def _simplify_claim(self, claim: str) -> str: | |
| stop_words = ['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', | |
| 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', | |
| 'could', 'may', 'might', 'can'] | |
| words = claim.lower().split() | |
| key_words = [w for w in words if w not in stop_words and len(w) > 2] | |
| return ' '.join(key_words[:5]) if key_words else claim | |
| # ============================== | |
| # Helper Functions | |
| # ============================== | |
| def determine_verdict_color(rating: str) -> str: | |
| rating_lower = rating.lower() | |
| true_keywords = ["true", "correct", "accurate", "mostly true", "verified", "confirmed"] | |
| false_keywords = ["false", "incorrect", "inaccurate", "mostly false", "debunked", "fake", "pants on fire"] | |
| mixed_keywords = ["mixture", "mixed", "partially", "misleading", "unproven", "undetermined"] | |
| if any(k in rating_lower for k in true_keywords): | |
| return "verdict-true" | |
| elif any(k in rating_lower for k in false_keywords): | |
| return "verdict-false" | |
| elif any(k in rating_lower for k in mixed_keywords): | |
| return "verdict-mixed" | |
| return "verdict-unverified" | |
| def get_verdict_emoji(rating: str) -> str: | |
| return {"verdict-true":"β ","verdict-false":"β","verdict-mixed":"β οΈ","verdict-unverified":"β"}.get(determine_verdict_color(rating),"β") | |
| def classify_rating(rating: str) -> str: | |
| r = rating.lower() | |
| if any(k in r for k in ["false","fake","pants on fire"]): return "false" | |
| if any(k in r for k in ["true","correct","accurate"]): return "true" | |
| if any(k in r for k in ["mixed","misleading","partially","half","mostly"]): return "mixed" | |
| return "unverified" | |
| # ============================== | |
| # Main App | |
| # ============================== | |
| def main(): | |
| st.markdown('<div class="main-header">π VeriFact</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sub-header">AI-Powered Fact Checking β’ Built by Areeba Fatima</div>', unsafe_allow_html=True) | |
| # Sidebar | |
| with st.sidebar: | |
| st.header("βοΈ Configuration") | |
| api_key_input = st.text_input("Google API Key (Optional)", type="password", help="Leave empty to use Hugging Face Secrets") | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div class="verdict-unverified"> | |
| <p>βΉοΈ <strong>VeriFact</strong> verifies claims using Google's Fact Check Tools API against | |
| databases from PolitiFact, Snopes, FactCheck.org, and 50+ other sources.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div class="verdict-unverified"> | |
| <p>βΉοΈ <strong>How to Use:</strong></p> | |
| <ul> | |
| <li>Enter any claim</li> | |
| <li>Click Verify</li> | |
| <li>Review results</li> | |
| <li>Check sources</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| with st.expander("π Example Claims to Try"): | |
| st.code(""" | |
| β TRUE: | |
| β’ Water boils at 100Β°C at sea level | |
| β’ Pakistan gained independence in 1947 | |
| β’ The Earth orbits the Sun | |
| β FALSE: | |
| β’ The Earth is flat | |
| β’ Vaccines cause autism | |
| β’ 5G causes COVID-19 | |
| β οΈ MIXED: | |
| β’ Coffee is good for health | |
| β’ Organic food is healthier | |
| """) | |
| # Initialize API | |
| api_key = api_key_input if api_key_input else None | |
| fact_checker = GoogleFactCheckAPI(api_key) | |
| # Main content | |
| st.markdown('<div class="section-title"><strong>π Enter Your Claim</strong></div>', unsafe_allow_html=True) | |
| claim_text = st.text_area("Claim to Verify", height=120, placeholder="Example: COVID-19 vaccines are safe and effective", help="Enter any factual claim you want to verify") | |
| col1, col2, col3 = st.columns([2,2,3]) | |
| with col1: verify_button = st.button("π Verify Claim", type="primary", use_container_width=True) | |
| with col2: clear_button = st.button("ποΈ Clear", use_container_width=True) | |
| if clear_button: st.rerun() | |
| # Process verification | |
| if verify_button: | |
| if not claim_text.strip(): | |
| st.markdown(""" | |
| <div class="verdict-mixed"> | |
| <p>β οΈ Please enter a claim to verify.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| with st.spinner("π Searching fact-check databases..."): | |
| start_time = time.time() | |
| results = fact_checker.verify_claim(claim_text) | |
| end_time = time.time() | |
| st.markdown("---") | |
| if results["status"] == "error": | |
| st.markdown(f""" | |
| <div class="verdict-false"> | |
| <p>β {results['message']}</p> | |
| </div> | |
| <div class="verdict-unverified"> | |
| <p>βΉοΈ <strong>Troubleshooting:</strong></p> | |
| <ul> | |
| <li>Make sure you added <code>GOOGLE_API_KEY</code> in Settings β Repository secrets</li> | |
| <li>Verify the API key is correct</li> | |
| <li>Ensure Fact Check Tools API is enabled in Google Cloud Console</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| elif results["status"] == "no_results": | |
| st.markdown(f""" | |
| <div class="verdict-mixed"> | |
| <p>β οΈ {results['message']}</p> | |
| </div> | |
| <div class="verdict-unverified"> | |
| <p>βΉοΈ <strong>Why no results?</strong></p> | |
| <ul> | |
| <li>This specific claim may not have been fact-checked yet</li> | |
| <li>Try rephrasing (simpler is better)</li> | |
| <li>Try well-known claims like "The Earth is flat"</li> | |
| <li>Check spelling</li> | |
| </ul> | |
| <p><strong>Note:</strong> Not all claims have fact-checks available. The database contains | |
| claims that have been verified by major fact-checking organizations.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| elif results["status"] == "success": | |
| st.markdown(f""" | |
| <div class="verdict-true"> | |
| <h3>β Found {results['count']} fact-check(s)</h3> | |
| <p>β± Time taken: {end_time - start_time:.2f}s</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown('<div class="section-title"><strong>π Results</strong></div>', unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="verdict-unverified"> | |
| <p>βΉοΈ <strong>Important:</strong> VeriFact does not perform original fact-checking. | |
| It retrieves claims that have already been verified by professional, | |
| published fact-checking organizations approved by Google.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| ratings = [r["rating"] for r in results["results"]] | |
| classified = [classify_rating(r) for r in ratings] | |
| true_count = classified.count("true") | |
| false_count = classified.count("false") | |
| mixed_count = classified.count("mixed") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: st.markdown(f"""<div class="stat-box"><h2 style="color:#28a745;">β {true_count}</h2><p>True/Accurate</p></div>""", unsafe_allow_html=True) | |
| with col2: st.markdown(f"""<div class="stat-box"><h2 style="color:#dc3545;">β {false_count}</h2><p>False/Incorrect</p></div>""", unsafe_allow_html=True) | |
| with col3: st.markdown(f"""<div class="stat-box"><h2 style="color:#ffc107;">β οΈ {mixed_count}</h2><p>Mixed/Misleading</p></div>""", unsafe_allow_html=True) | |
| with col4: st.markdown(f"""<div class="stat-box"><h2 style="color:#6c757d;">π° {results['count']}</h2><p>Total Sources</p></div>""", unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.markdown('<div class="section-title"><strong>π Detailed Fact-Check Results</strong></div>', unsafe_allow_html=True) | |
| for idx, result in enumerate(results["results"], 1): | |
| verdict_class = determine_verdict_color(result["rating"]) | |
| emoji = get_verdict_emoji(result["rating"]) | |
| st.markdown(f""" | |
| <div class="{verdict_class}"> | |
| <h3>{emoji} Fact-Check #{idx}</h3> | |
| <p><strong>Rating:</strong> {result["rating"]}</p> | |
| <p><strong>Publisher:</strong> {result["publisher"]}</p> | |
| <p><strong>Claim:</strong> {result["claim_text"]}</p> | |
| <p><strong>Claimant:</strong> {result["claimant"]}</p> | |
| <p><strong>Date:</strong> {result["claim_date"]}</p> | |
| <p><strong>Title:</strong> {result["title"]}</p> | |
| <p><strong>Source:</strong> <a href="{result["url"]}" target="_blank">View Full Fact-Check β</a></p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Export Report | |
| st.markdown("---") | |
| st.markdown('<div class="section-title"><strong>π Report</strong></div>', unsafe_allow_html=True) | |
| export_text = f"VERIFACT - CLAIM VERIFICATION REPORT\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\nCLAIM: {claim_text}\n\nSUMMARY:\n- Total Fact-Checks: {results['count']}\n- True/Accurate: {true_count}\n- False/Incorrect: {false_count}\n- Mixed/Misleading: {mixed_count}\n\nDETAILED RESULTS:\n" | |
| for idx, result in enumerate(results["results"],1): | |
| export_text += f"\n{idx}. {result['publisher']}\n Rating: {result['rating']}\n URL: {result['url']}\n Claimant: {result['claimant']}\n Date: {result['claim_date']}\n Title: {result['title']}\n" | |
| st.download_button( | |
| label="π Download Report", | |
| data=export_text, | |
| file_name=f"verifact_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", | |
| mime="text/plain", | |
| use_container_width=True | |
| ) | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style="text-align:center;color:#666;padding:2rem 0;"> | |
| <p style="font-size:1.1rem;"><strong>VeriFact</strong> | Powered by Google Fact Check Tools API</p> | |
| <p>Built by <strong>Areeba Fatima</strong> with Python & Streamlit</p> | |
| <p style="font-size:0.85rem;color:#999;">β οΈ Always verify important claims from multiple sources. This tool is for informational purposes.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if __name__ == "__main__": | |
| main() | |