# Project export: Automated Fact Checker

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: Simply post an article or youtube URL, type in a keyword to narrow down the content into focused claims, and watch as every opinionated claim is presented to the user with sources and a yes/no rating.
- Devpost: https://devpost.com/software/automated-fact-checker
- GitHub: https://github.com/Tyler-Dunning/fact-checker-api/
- Demo: https://github.com/PhongDiep2003/fact-checker-frontend
- Team: 1 GitHub contributor(s) — Tyler-Dunning (1 commits)

## Devpost submission (written by the team)

### Inspiration

So much misinformation is being spread on the internet, most of which can quickly be disproven with a quick, researched article.

### What it does

Automated Fact Checker takes in any URL to an article or Youtube video along with a key word to specify what type of claims the user wants to see. From this, the program will generate a list of every claim that relates to the specified key word, sources that provide further context of each claim, and a determination of whether the claim is true or false. Users will have the option to post their results, as well as view, like, and comment on other user's posts.

### How we built it

We built a Python API using Flask that takes the URL and key word as arguments, and runs all the logic for creating the result with the claims, sources, and determination. This API is hosted on AWS EC2 so that it can be accessed publicly. We built the web interface in Next with JavaScript. It is hosted on Vercel, and connects with our MongoDB schema that stores user and post data.

### Challenges we ran into

Trying to create ways to implement our dream while still being realistic for the time constraint was challenging. Integrating the different components we all made without having a long time to discuss made it difficult to seamlessly merge our work. Learning many new technologies, frameworks, and libraries in a short time period was challenging, and let to errors down the line.

### Accomplishments we're proud of

We are proud that we were able to make this app (almost) fully functioning. We knew that our goal was ambitious, but we persisted through the endless bugs and challenges that came our way. While the product is not as perfect as we would like it to be, it is still a huge step in the right direction for our goal.

### What we learned

We learned how to work on a time crunch. We learned more about realistic expectations and budgeting time to tackle important tasks first.

### What's next

We would like to improve the language processing to better identify claims and keywords. Getting the site fully functioning and operational on a public domain would be a huge accomplishment. Making steps towards making the logic more precise in finding sources that relate to the claims could be a huge benefit to users.

## README (from the GitHub repository)

This API is built to interact with the "Automated Fact Checker" program.

Includes a Python/Flask API designed to be hosted on an AWS EC2 instance.

It accepts HTTP GET requests to <IP>/check with params url & phrase.

This GET request will return a JSON object containing all of the identified claims within the article/Youtube video of the given URL that relate to the given phrase.
Each claim will be accompanied by three sources, as well as an assessment of if the sources support the claim.
The format is as follows:
{
  {success: true} 
  {claim: "Claim 1", sources: ["Source URL 1", "Source URL 2", "Source URL 3"], rating: "Assessment"} 
  {claim: ... }
}


## Detected evidence (automated analysis)

Indexed codebase: 2 recognized source files, 5 KB.
- Flask (technology) — detected in the code
- Python (language) — detected in the code
- AWS (technology) — claimed on Devpost, not found in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- MongoDB (technology) — claimed on Devpost, not found in the code
- Next.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (4 of 4)

```
.gitignore
app.py
README.md
requirements.txt
```

### Dependencies

- requirements.txt: beautifulsoup4@==4.12.3, Flask@==3.0.3, Flask-Cors@==5.0.0, google-generativeai@==0.8.3, requests@==2.32.2, spacy@==3.8.2, urllib3@==1.26.18, yake@==0.4.8, youtube-transcript-api@==0.6.2

### Recent commits (newest first)

- Create README.md
- envs
- with reqs
- Switchup

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
youtube-transcript-api==0.6.2
yake==0.4.8
urllib3==1.26.18
spacy==3.8.2
requests==2.32.2
google-generativeai==0.8.3
Flask==3.0.3
Flask-Cors==5.0.0
beautifulsoup4==4.12.3
```

### app.py

```python
from flask import Flask, request
import spacy
from spacy.lang.en.stop_words import STOP_WORDS
import requests
from bs4 import BeautifulSoup
from youtube_transcript_api import YouTubeTranscriptApi
from urllib.parse import urlparse, parse_qs
import json
import google.generativeai as genai
from flask_cors import CORS, cross_origin
import os


genai.configure(api_key=os.getenv("GEMINI_API_KEY"))


def get_video_id(youtube_url):
    parsed_url = urlparse(youtube_url)
    video_id = parse_qs(parsed_url.query).get('v')
    
    return video_id[0] if video_id else None

def fetch_youtube_transcript(video_url):
    video_id = get_video_id(video_url)
    if not video_id:
        return "Invalid YouTube URL."
    
    try:
        transcript = YouTubeTranscriptApi.get_transcript(video_id)

        transcript_text = ' '.join([item['text'] for item in transcript])
        return transcript_text
    
    except Exception as e:
        return f"Error retrieving transcript: {e}"

def get_article_content(url):
    if "youtube.com" in url or "youtu.be" in url:
        return fetch_youtube_transcript(url)
    else:
        response = requests.get(url)

        if response.status_code != 200:
            return f"Failed to retrieve the article. Status code: {response.status_code}"
        
        soup = BeautifulSoup(response.text, 'html.parser')

        article = ""
        article_tags = soup.find_all('article')
        if article_tags:
            article = ' '.join([tag.get_text() for tag in article_tags])
        
        if not article:
            paragraphs = soup.find_all('p')
            article = ' '.join([p.get_text() for p in paragraphs])
        
        return article.strip().replace("\n", "").replace("\t", "")

# Load spaCy model
nlp = spacy.load("en_core_web_sm")

EXCLUDE_POS = ["PRON", "AUX", "DET", "PUNCT", "CCONJ", "ADP"] 
EXCLUDE_LEMMAS = ["be", "do", "have", "will", "can", "may"]

def extract_claims_and_keywords(text, topic):
    doc = nlp(text)
    claims_with_keywords = []

    for sent in doc.sents:
        if topic.lower() in sent.text.lower():
            keywords = []
            for token in sent:
                if (
                    token.pos_ not in EXCLUDE_POS
                    and token.lemma_ not in EXCLUDE_LEMMAS
                    and token.text.lower() not in STOP_WORDS
                    and token.is_alpha
                ):
                    keywords.append(token.text)

            claims_with_keywords.append({
                "claim": sent.text,
                "keywords": keywords[:7]
            })

    return claims_with_keywords

def get_sources_from_claims(claims):
    all_sources = []
    for claim in claims:
        claim_str = ""
        for c in claim:
            claim_str += c + " | "
        params = {'api_token': os.getenv("NEWS_API_KEY"),
                'search': claim_str}
        result_json = requests.get('https://api.thenewsapi.com/v1/news/all?language=en&limit=3&', params=params).json()

        sources = []
        for z in result_json['data']:
            sources.append(z['url'])
        all_sources.append(sources)

    return all_sources

def check_claims_by_source(claims, sources):
    model = genai.GenerativeModel("gemini-1.5-flash")
    results = []
    for i in range(len(claims)):
        claim = claims[i]
        source = sources[i][0]
        results.append(model.generate_content(f"Tell me if the claim {claim} is supported by the following article, reply with ONLY 'True', 'False', 'Misleading', or 'Unsure' and then say a percent certainty: {get_article_content(source)}").text[0:-2])
    return results


app = Flask(__name__)
cors = CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'

@app.route('/check')
@cross_origin()
def home():
    print("request received")
    url = request.args.get('url')
    phrase = request.args.get('phrase')
    article = ""
    try:
        article = get_article_content(url)
    except:
        return json.dumps({"success": "false"})
    print("article read")
    claims_keywords = extract_claims_and_keywords(article, phrase)
    claims = []
    kws = []
    for ck in claims_keywords:
        claims.append(ck["claim"])
        kws.append(ck["keywords"])

    print("claims found")
    sources = get_sources_from_claims(kws)
    print("sources found")
    verifications = check_claims_by_source(claims, sources)
    print("verified")
    print(verifications)
    result = []
    result.append({"success": "true"})
    for claim, source, verification in zip(claims, sources, verifications):
        result.append({
            "text": claim,
            "sources": source,
            "rating": verification
        })
    json_res = json.dumps(result, indent=4)
    print("results generated")
    return json_res


if __name__ == '__main__':
    app.run()
```