# Project export: GenuisAI

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: TreeHacks 2025
- Tagline: Students deserve to study with confidence, accuracy, and trust. That's why Genuis is more than just smart - it's genuine.
- Devpost: https://devpost.com/software/genuius-your-genuine-genius
- GitHub: https://github.com/fabianmolinaEE/TreeHacks25
- Demo: https://app.flutterflow.io/run/d3DbI94DtyxW5RVApBvE
- Video: https://www.youtube.com/embed/ica8KIuQ_S4?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Fabian Molina (7 commits), Efrain Angon (5 commits), Suprad S Parashar (3 commits)

## Devpost submission (written by the team)

### Inspiration

The last thing you want your tutor to do is lie to you. Yet, 77% of users have been deceived by AI hallucinations when using chatbots for learning (Tidio). Students deserve to study with confidence, accuracy, and trust, but traditional AI tutors often fabricate information, leading to confusion and misinformation. We built Genuis because students shouldn’t have to second-guess and cross-reference their study tools. Unlike general AI models, which prioritize versatility over accuracy, Genuis only provides answers grounded in real course materials and lectures. Whether you're reviewing for an exam or tackling a tough assignment, Genuis ensures that your AI tutor is as reliable as your professor.

### What it does

Genuis is an AI-powered study assistant that delivers fact-checked, course-specific tutoring by pulling directly from uploaded lecture notes, textbooks, and classroom materials. It never hallucinates answers—it simply doesn't generate responses unless it finds relevant context in your uploaded resources. And for those tired of copying and pasting to feed a chat bot content you want clarification on—we do exactly that! We keep an extensive database of all the prompts you’ve entered previously which is often refactored and holds on to the contents of pdfs, html, and even youtube videos that can deliver your answers and thorough understanding. Ensuring your minimal input, the database considers specific courses, professors, office hours, assignments, lectures, and much more so that you get answers at the click of a button. And the icing on the cake—our voice feature with vocal output option that makes it so you don’t even have to read! Challenges We're Proud we Handled 1. Efficient Data Retrieval with Elastic Database One of our biggest challenges was efficiently keeping track of past user prompts and their associated resources. To solve this, we leveraged an Elastic database that stored:Past user prompts, the most relevant link associated with each prompt, and the type of link (PDF, HTML, YouTube, etc.) To optimize retrieval, we vectorized both user prompts and the content of stored links. When a new query came in, we used cosine similarity (a vector comparison method) and vector regression analysis to compare the new query against existing prompt vectors and content vectors in our database. If we had enough relevant information, we could provide an answer without redundant scraping, significantly improving efficiency. 2. Deciding When to Search vs. Answer Directly A major design challenge was determining whether to: Let the LLM answer a question based on its existing knowledge. Search our database for relevant information. Scrape new data from the web. To solve this, we had the LLM rate its confidence (on a scale from 1 to 10) regarding its ability to answer a given question without additional context. If the confidence score was below 8, we either searched our database or scraped new data. This approach minimized unnecessary API calls and improved overall runtime efficiency. 3. Finding the Most Relevant Link When multiple potential sources were available, we needed a way to determine the best link for answering a user's question. We solved this by leveraging Perplexity’s Sonar API, which intelligently ranked links based on relevance. This ensured that users received the most accurate and high-quality information available. Advantages You Can Get Behind 🔍 Smarter Than Your Browser Tabs No more endless Googling, clicking on sketchy forum posts, or drowning in 50 open tabs. Our AI-powered search actually understands your question and fetches the most relevant answers—instantly. ⚡ Speed Demon – Thanks to our universal vector database magic and cosine similarity wizardry, we don’t waste time scraping the web if we already have what you need. That means less waiting, more learning. 🗣️ Talks Back to You! Our dynamic text-to-speech feature lets your device read out responses, making interactions more fun and accessible! 🧠 Memory of an Elephant – Unlike search engines that forget you exist after each query, our assistant remembers past interactions. It can recall what you’ve asked before, suggest better resources, and even track your academic progress over time. 🤖 AI That Knows When to Chill – Ever seen an AI panic-search for answers it already knows? Not here. Our LLM is self-aware enough to rate its confidence and only fetch external data when necessary. 📚 PDFs, YouTube, and More—Oh My! – Whether it’s lecture slides, online textbooks, or video tutorials, we go beyond basic web pages. Even that ancient PDF your professor uploaded in 2013 isn’t safe from our AI’s retrieval powers. 📅 Your Study Buddy (Who Actually Cares) – We’re not just here to answer questions—we organize your learning. Need to review circuits before your exam? Our system can suggest study sessions, set up meetings with advisors, and even integrate with your calendar. AI-powered academic life-hacks, anyone? 💡 Not Just Smarter—Getting Smarter – The more you use it, the better it gets at knowing what you need. With ongoing ranking improvements and real-time feedback, this isn’t just a tool—it’s an evolving study companion, who genuinely wants to see you thrive.

### What we learned

Optimizing Efficiency A key challenge was balancing response speed and accuracy. To optimize this, we implemented a confidence-based retrieval system, where the LLM rated its ability to answer a query without additional context. If the confidence was below 8, the system either queried our vector database or performed a web search, reducing unnecessary API calls and improving runtime efficiency. To prevent redundant data retrieval, we leveraged Elasticsearch to store past prompts, relevant links, and vectorized content. Using cosine similarity, we compared new user queries with stored data to determine whether an existing answer was sufficient before scraping new information. This approach significantly reduced latency and minimized redundant external requests. Tools & Design Choices We designed our system using FlutterFlow for the front end and Python APIs for backend processing. Key tools and integrations included: OpenAI API for natural language processing and query understanding Perplexity Sonar API for selecting the most relevant sources Text-to-Speech APIs to enhance accessibility Mathematical methods (vectorization, cosine similarity) for ranking query relevance Elasticsearch for efficient data retrieval and indexing These choices allowed us to create a system that prioritizes efficiency while maintaining high accuracy. By combining structured data storage, intelligent retrieval methods, and API-driven search capabilities, we ensured a fast, relevant, and user-friendly experience.

### What's next

for Genuis - Your Genuine Genius One of our main goals moving forward is to expand how we use the vector database to provide a more personalized and proactive academic assistant. Currently, we store past user prompts and relevant resources, but we want to take this further by organizing entire conversations to track student learning progress over time. By analyzing a student’s search history and previous interactions, our system could: Periodically update conditionals based on their coursework and study patterns. Suggest study sessions when it detects gaps in understanding. Recommend meeting with an advisor if it notices frequent queries related to struggling topics or career planning. Integrate with calendars to automatically set up study sessions, tutoring, or academic advising meetings based on workload and deadlines.

## README (from the GitHub repository)

# TreeHacks25
Repo for treehacks 2025. Fabian, Efrain, Amesha, Suprad

Demo Video: https://www.youtube.com/watch?v=ica8KIuQ_S4 

🧠 Study Smarter, Not Harder
GenuisAI is an AI-powered academic assistant designed to eliminate AI hallucinations, ensuring students get fact-checked, course-specific answers. Unlike generic AI tutors, GenuisAI pulls directly from uploaded lecture notes, textbooks, and course materials—delivering accurate, reliable study support.

🚀 Features
✅ Fact-Checked AI Tutoring – Answers are only provided if relevant course material is available.
✅ Personalized Learning – Stores past queries and resources for efficient recall.
✅ Multi-Source Retrieval – Supports PDFs, HTML, YouTube videos, and textbooks.
✅ Smart Query Optimization – Uses vectorization, cosine similarity, and ranking algorithms to fetch the best resources.
✅ Text-to-Speech Support – Hands-free, vocalized responses for accessibility.
✅ Efficient Data Storage – Powered by Elasticsearch and optimized with Perplexity’s Sonar API.
✅ Self-Aware AI – Rates its confidence before answering to minimize misinformation.

🛠 Tech Stack
Frontend: FlutterFlow
Backend: Python APIs, OpenAI API, Elasticsearch, Groq
AI & Data Retrieval: OpenAI, Perplexity Sonar API, Whisper, ElevenLabs
Hosting & Infrastructure: AWS
🎯 Challenges Solved
Efficient Data Retrieval – Using an Elastic database, vectorized prompts, and cosine similarity for optimal response time.
Smart Query Decisions – AI determines whether to answer, search the database, or scrape new data.
Optimal Source Selection – Integrates Perplexity’s Sonar API for ranking relevant sources.
🔮 Future Plans
Personalized Learning Insights – Track study patterns and suggest improvement areas.
Automated Study Scheduling – Integrate with calendars to schedule tutoring/study sessions.
Enhanced Course Tracking – Provide tailored resources based on a student’s academic progress.


## Detected evidence (automated analysis)

Indexed codebase: 15 recognized source files, 58 KB.
- Python (language) — detected in the code
- AWS (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (15 of 15)

```
__init__.py
api.py
api2.py
elasticDump.py
ElasticSearch_Comparison.py
ocr.py
perplexity.py
PerplexityJSONDump.py
README.md
scrapybara_openai_pipeline.py
stt.py
testingnewmethod.py
tts.py
WorkingJSON_Dump.py
zoom.py
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Update README.md
- Update README.md
- Update ElasticSearch_Comparison.py
- Update README.md
- Remove API Keys
- Update elasticDump.py
- Update elasticDump.py
- Remove API Key
- Update perplexity.py
- Separate the API for content and add confidence level.
- Add API Endpoint for Recommendations
- Modify API to handle non-context queries.
- Modify API for content
- Add API for content
- vector comparison elastic search
- vector draft of elasticDump
- Create API to access Perplexity Function
- new version of perplexity JSON dump
- your changes
- Merge pull request #2 from fabianmolinaEE/fabian

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

### zoom.py

```python
# import jsonDumpV2 as perplexity

# link = perplexity.get_perplexity_response("What is the email address of the professor who teaches CS111 at Stanford?")
# response = perplexity.check_link_content(link)

# print(response)
```

### stt.py

```python
from openai import OpenAI
import dotenv
import os

dotenv.load_dotenv()

client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
)

# Testing with a sample audio file.
audio_file= open("harvard.wav", "rb")
transcription = client.audio.transcriptions.create(
    model="whisper-1", 
    file=audio_file
)

print(transcription.text)
```

### tts.py

```python
import dotenv
from elevenlabs.client import ElevenLabs
from elevenlabs import play
import os

dotenv.load_dotenv()

client = ElevenLabs(
    api_key=os.getenv("ELEVENLABS_API_KEY"),
)

audio = client.text_to_speech.convert(
    text="The first move is what sets everything in motion. The quick brown fox jumps over the lazy dog.",
    voice_id="JBFqnCBsd6RMkjVDRZzb",
    model_id="eleven_multilingual_v2",
    output_format="mp3_44100_128",
)
play(audio)
```

### ocr.py

```python
# Pipeline - 
# Download PDF
# Convert PDF to Image
# Extract Text from Image

import os
import requests
import pdf2image
import easyocr

def download_pdf(url):
    response = requests.get(url)
    with open("file.pdf", "wb") as file:
        for chunk in response.iter_content(chunk_size=8192):
            file.write(chunk)

def convert_pdf_to_image():
    pages = pdf2image.convert_from_path("file.pdf", 500)
    for i, page in enumerate(pages):
        page.save(f'page_{i}.jpg', 'JPEG')
    return len(pages)

def extract_text_from_image():
    reader = easyocr.Reader(['en'])
    text = ""
    for i in range(convert_pdf_to_image()):
        output = reader.readtext(f'page_{i}.jpg')
        for data in output:
            text += data[1] + " "
        os.remove(f"page_{i}.jpg")
    os.remove("file.pdf")
    print("Text extracted successfully!")
    return text

def read_pdf(url):
    download_pdf(url)
    return extract_text_from_image()
```

### testingnewmethod.py

```python
import requests
import fitz  # PyMuPDF

def download_pdf(url, save_path):
    """Downloads a PDF from a given URL and saves it to a file."""
    response = requests.get(url, stream=True)
    if response.status_code == 200:
        with open(save_path, "wb") as file:
            for chunk in response.iter_content(chunk_size=8192):
                file.write(chunk)
        print(f"✅ PDF downloaded successfully: {save_path}")
    else:
        print(f"❌ Failed to download PDF. Status code: {response.status_code}")
        return None

def extract_text_pymupdf(pdf_path):
    """Extracts text from a PDF file using PyMuPDF."""
    doc = fitz.open(pdf_path)
    text = "\n".join([page.get_text() for page in doc])
    return text

# Define the URL and file path
pdf_url = "https://web.stanford.edu/class/archive/cs/cs111/cs111.1254/lectures/10/Lecture10.pdf"
pdf_path = "lecture10.pdf"

# Download and extract text
download_pdf(pdf_url, pdf_path)
extracted_text = extract_text_pymupdf(pdf_path)

# Print extracted text
print("\n📄 Extracted Text:\n")
print(extracted_text)
```

### api2.py

```python
from fastapi import FastAPI
from . import perplexity
from elasticsearch import Elasticsearch
import os
import dotenv
from sentence_transformers import SentenceTransformer
from groq import Groq
from . ElasticSearch_Comparison import check_information_sufficiency
import requests
from . import api

dotenv.load_dotenv()

app = FastAPI()

@app.get("/content")
def search(q: str):
    client = Groq(
        api_key=os.getenv("GROQ_API_KEY"),
    )
    chat_response = client.chat.completions.create(
        messages=[{
        "role": "user",
        "content": f"For the following question, how well do you think you can answer this prompt without additional context between 1 to 10 with 10 being easily answerable. Just return the number and nothing else.\n\n{q}"
    }],
        model="llama-3.3-70b-versatile"
    )
    response = chat_response.choices[0].message.content
    print(response)
    if int(response) > 7:
        return ""
    has_sufficient_info, relevant_doc = check_information_sufficiency(q)
    if has_sufficient_info:
        return relevant_doc["_source"]["content"]
    return api.read_root(q)["content"]

```

### PerplexityJSONDump.py

```python
from openai import OpenAI
import requests
from bs4 import BeautifulSoup
import json
import re
import ocr

# Initialize the OpenAI client with Perplexity API
client = OpenAI(api_key=os.getenv("OPEN_AI_KEY"), base_url="https://api.perplexity.ai")

def get_perplexity_response(prompt):
    i = 0
    while i < 3:
        """Gets a response from Perplexity API and extracts the first link."""
        messages = [
            {"role": "system", "content": "You are to return only the most relevant link to the prompt. Do not include any other text in your response. Make sure that the link returned is specific to the prompt."},
            {"role": "user", "content": prompt}
        ]
        
        response = client.chat.completions.create(
            model="sonar-reasoning-pro",
            messages=messages,
            max_tokens=1024
        )
        
        content = response.choices[0].message.content.strip()
        print(content)
        # Use regex to extract URL
        url_pattern = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
        match = url_pattern.search(content)
        if(match):
            return match.group(0)
        i += 1

def check_link_content(link):
    # print(link)

    """Determines whether the link is a PDF or plaintext and extracts content if plaintext."""
    if not link:
        return {"link": "No link found.",
                "type": "Error: No link found"}
    
    headers = {"User-Agent": "Mozilla/5.0"}
    response = requests.get(link, headers=headers, stream=True)
    i = 0
    while i < 3:
        response = requests.get(link, headers=headers, stream=True)
        if response.status_code == 200:
            break
        i += 1
    else: 
        return {"link": "No link found.",
                "type": "Error: Cannot retrieve link"}
    
    content_type = response.headers.get("Content-Type", "").lower()

    if "application/pdf" in content_type:
        text = ocr.read_pdf(link)
        return {"link": link, "type": "pdf", "content": text}
    
    elif "text/html" in content_type:
        soup = BeautifulSoup(response.content, "html.parser")
        text = " ".join([p.text for p in soup.find_all(["p", "h1", "h2", "h3", "h4", "h5", "h6", "li"])])
        return {"link": link, "type": "plaintext", "content": text}
    
    else:
        return {"link": link, 
                "type": "Error: Unsupported content type."}
    
        

def main():
    user_input = input("Enter your homework question or topic: ")
    
    # Get response from Perplexity API
    link = get_perplexity_response(user_input)
    
    # Check link content type and extract information
    response = check_link_content(link)

    print(json.dumps(response, indent=4))

if __name__ == "__main__":
    main()

```

### api.py

```python
from fastapi import FastAPI
from . import perplexity
from elasticsearch import Elasticsearch
import os
import dotenv
from sentence_transformers import SentenceTransformer
from groq import Groq
from . ElasticSearch_Comparison import check_information_sufficiency
import requests

dotenv.load_dotenv()

app = FastAPI()

@app.get("/perplexity")
def read_root(q: str):
    link = perplexity.get_perplexity_response(q)
    response = perplexity.check_link_content(link)

    client = Elasticsearch(
        "https://my-elasticsearch-project-faf96a.es.us-east-1.aws.elastic.cloud",
        api_key=os.getenv("ELASTIC_API_KEY"),
    )
    model = SentenceTransformer('all-MiniLM-L6-v2')
    content_vector = model.encode(response["content"]).tolist()
    payload = {
        "prompt": q,
        "content": response["content"],
        "content_vector": content_vector,
        "prompt_vector": model.encode(q).tolist(),
        "link": link,
    }
    client.index(index="data_dump", body=payload)
    return response

@app.get("/content")
def search(q: str):
    client = Groq(
        api_key=os.getenv("GROQ_API_KEY"),
    )
    chat_response = client.chat.completions.create(
        messages=[{
        "role": "user",
        "content": f"For the following question, reply with a 'Yes' if you need specific information to answer the question, otherwise, reply with a 'No'.\n\n{q}"
    }],
        model="llama-3.3-70b-versatile"
    )
    response = chat_response.choices[0].message.content
    if "No" in response:
        return ""
    has_sufficient_info, relevant_doc = check_information_sufficiency(q)
    if has_sufficient_info:
        print("Sufficient information found.")
        return relevant_doc["_source"]["content"]
    print("Getting information from Perplexity API.")
    return read_root(q)["content"]
    
@app.get("/recommendations")
def get_relevant_links(prompt: str):
    headers = {
        "Authorization": f"Bearer {os.getenv("PERPLEXITY_API_KEY")}",
        "Content-Type": "application/json"
    }
    data = {
        "model": "llama-3.1-sonar-small-128k-online",
        "messages": [
            {"role": "system", "content": "You are to only provide links related and relevant to the prompt. Format each link as a python string with double quotes at the beginning and end of each string. Put each url in a new line. No other unnecessary words or characters are allowed"},
            {"role": "user", "content": f"Provide 3-5 relevant and trustworthy links for: {prompt} and Don't use any other characters besides what is necessary."}
        ]
    }
    try:
        response = requests.post("https://api.perplexity.ai/chat/completions", headers=headers, json=data)
        response.raise_for_status()
        result = response.json()
        links = result['choices'][0]['message']['content'].split('\n')
        return [link.strip() for link in links if link.strip()]
    except requests.exceptions.RequestException as e:
        return []

```

### ElasticSearch_Comparison.py

```python
from elasticsearch import Elasticsearch
from sentence_transformers import SentenceTransformer
import numpy as np
from scipy.spatial.distance import cosine

# Initialize Elasticsearch client
es = Elasticsearch(
    "https://my-elasticsearch-project-faf96a.es.us-east-1.aws.elastic.cloud",
    api_key="testing=="
)

# Initialize sentence transformer model
model = SentenceTransformer('all-MiniLM-L6-v2')

def cosine_similarity(vec1, vec2):
    return 1 - cosine(vec1, vec2)

def check_information_sufficiency(prompt, threshold=0.65):
    # Encode the prompt into an embedding vector
    prompt_embedding = model.encode(prompt)
    # Elasticsearch query using script_score for content_vector similarity
    search_body = {
        "size": 10,  # Retrieve top 10 documents
        "query": {
            "script_score": {
                "query": {"match_all": {}},
                "script": {
                    "source": "cosineSimilarity(params.query_vector, 'content_vector') + 1.0",
                    "params": {"query_vector": prompt_embedding.tolist()}
                }
            }
        }
    }

    try:
        # Perform search in Elasticsearch
        results = es.search(index="data_dump", body=search_body)
        
        max_similarity = -1
        most_similar_doc = None

        for hit in results['hits']['hits']:
            content_vector = np.array(hit['_source']['content_vector'])
            prompt_vector = np.array(hit['_source'].get('prompt_vector', []))
            
            content_similarity = cosine_similarity(prompt_embedding, content_vector)
            
            # If prompt_vector exists, calculate its similarity, otherwise use content_similarity
            if prompt_vector.size > 0:
                prompt_similarity = cosine_similarity(prompt_embedding, prompt_vector)
                combined_similarity = (content_similarity + prompt_similarity) / 2
            else:
                combined_similarity = content_similarity
            
            if combined_similarity > max_similarity:
                max_similarity = combined_similarity
                most_similar_doc = hit
        print(combined_similarity)
        # Determine if the max similarity meets the threshold
        print(max_similarity)
        if max_similarity >= threshold:
            return True, most_similar_doc
        else:
            return False, None

    except Exception as e:
        print(f"Error during Elasticsearch query: {e}")
        return False, []

# # Example usage
# prompt = ["I am taking CS111 at Stanford. Could you summarize lecture 10?", "I want a hamburger", "I want to take CS111", "Could you do lecture 10", "I want to understand lecture 10 of CS111 at Stanford better.", "I am taking CS111 at Stanford. Can you summarize the syllabus", "What are the office hours for the proffessor who teaches CS111 Stanford"]
# for pr in prompt:
#     has_sufficient_info, relevant_docs, content = check_information_sufficiency(pr)

#     if has_sufficient_info:
#         print("We have sufficient information to answer this prompt.")
#         for doc in relevant_docs:
#             print(f"Document ID: {doc['_id']}, Score: {doc['_score']}")
#     else:
#         print("We don't have enough information. Let's scrape the web or use an external API.")


```

### perplexity.py

```python
from openai import OpenAI
import requests
from bs4 import BeautifulSoup
import json
import re
from . import ocr
import fitz 
import os
from youtube_transcript_api import YouTubeTranscriptApi
from urllib.parse import urlparse, parse_qs

# Initialize the OpenAI client with Perplexity API
client = OpenAI(api_key="hideenloll", base_url="https://api.perplexity.ai")

def get_perplexity_response(prompt):
    i = 0
    while i < 3:
        """Gets a response from Perplexity API and extracts the first link."""
        messages = [
            {"role": "system", "content": "You are to return only the most relevant link to the prompt. Do not include any other text in your response. Make sure that the link returned is specific to the prompt."},
            {"role": "user", "content": prompt}
        ]
        
        response = client.chat.completions.create(
            model="sonar-reasoning-pro",
            messages=messages,
            max_tokens=1024
        )
        
        content = response.choices[0].message.content.strip()
        print(content)
        # Use regex to extract URL
        url_pattern = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
        match = url_pattern.search(content)
        if(match):
            return match.group(0)
        i += 1

def check_link_content(link):
    # print(link)

    """Determines whether the link is a PDF or plaintext and extracts content if plaintext."""
    if not link:
        return {"link": "No link found.",
                "type": "Error: No link found"}
    
    headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"}
    i = 0
    while i < 50:
        response = requests.get(link, headers=headers, stream=True)
        if response.status_code == 200:
            break
        i += 1
    else: 
        return {"link": "No link found.",
                "type": "Error: Cannot retrieve link"}
    
    content_type = response.headers.get("Content-Type", "").lower()
    parsed_url = urlparse(link)
    if 'youtube.com' in parsed_url.netloc:
        try:
            # Try to get 'v' parameter from query string
            v = parse_qs(parsed_url.query).get('v')
            if v:
                id = v[0]
            
            print(id)
            transcript = YouTubeTranscriptApi.get_transcript(id)
            text = "\n".join([entry['text'] for entry in transcript])
            return {"link": link, "type": "video", "content": text}
        except Exception as e:
            return {"link": link, "type": "Error: No Transcript Found" , "content": "N/A"}

    if "application/pdf" in content_type:
        with open('./file.pdf', "wb") as file:
            for chunk in response.iter_content(chunk_size=8192):
                file.write(chunk)
        print(f"✅ PDF downloaded successfully: {'./'}")
        doc = fitz.open('./file.pdf')
        text = "\n".join([page.get_text() for page in doc])
        return {"link": link, "type": "pdf", "content": text}
    
    elif "text/html" in content_type:
        soup = BeautifulSoup(response.content, "html.parser")
        text = " ".join([p.text for p in soup.find_all(["p", "h1", "h2", "h3", "h4", "h5", "h6", "li"])])
        return {"link": link, "type": "plaintext", "content": text}
    
    else:
        return {"link": link, 
                "type": "Error: Unsupported content type."}
    
        

def main():
    user_input = input("Enter your homework question or topic: ")
    
    # Get response from Perplexity API
    link = get_perplexity_response(user_input)
    
    # Check link content type and extract information
    response = check_link_content(link)

    print(json.dumps(response, indent=4))
    if os.path.exists('./file.pdf'):
        os.remove('./file.pdf')
if __name__ == "__main__":
    main()

```

[3 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]