# Project export: Voice of Reason

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 2026
- Tagline: Instant fact-checking for YouTube videos. Verify claims in real-time as you watch.
- Devpost: https://devpost.com/software/voice-of-reason-9qkfxa
- GitHub: https://github.com/lc0001coll/ytFactChecker_TH2026
- Video: https://www.youtube.com/embed/8FfEMWZKwdQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Luis C (21 commits), Joziah Uribe (3 commits)

## Devpost submission (written by the team)

### Inspiration

We’ve all been there: halfway through a compelling video about health, finance, or global politics, wondering, "Is this actually true?" YouTube is the world's second-largest search engine, yet it remains a frontier for unchecked misinformation. Unlike platforms with Community Notes or news articles that can be easily cross-referenced, video content is often "locked." Manually pausing a video to search for a creator's claim is tedious and breaks the viewing experience. We built Voice of Reason to bridge that gap, providing a real-time "truth layer" over the videos we consume daily.

### What it does

Voice of Reason is a Chrome extension that acts as an AI-powered moderator for your YouTube experience. Check This Moment: With a single click (or ⌘⇧V / Ctrl+Shift+V), the AI analyzes the last 60 seconds of the video to verify the specific claim you just heard. Check This Moment: With a single click (or ⌘⇧V / Ctrl+Shift+V), the AI analyzes the last 60 seconds of the video to verify the specific claim you just heard. Full Video Analysis: Scans the entire transcript to generate a comprehensive report of all major claims. Full Video Analysis: Scans the entire transcript to generate a comprehensive report of all major claims. Visual Verdicts: Claims are tagged as TRUE, FALSE, or MISLEADING with concise reasoning and citations. Visual Verdicts: Claims are tagged as TRUE, FALSE, or MISLEADING with concise reasoning and citations. Interactive Timestamps: Click any claim in the report to jump directly to that moment in the video. Interactive Timestamps: Click any claim in the report to jump directly to that moment in the video. Authenticity Score: Provides an overall "Trust Rating" for the video based on the ratio of verified vs. debunked claims. Authenticity Score: Provides an overall "Trust Rating" for the video based on the ratio of verified vs. debunked claims. Deep Dive Chat: Not satisfied? Use the built-in chat to ask follow-up questions about the sources or context directly within the extension. Deep Dive Chat: Not satisfied? Use the built-in chat to ask follow-up questions about the sources or context directly within the extension.

### How we built it

We combined a high-performance Python backend with a reactive Chrome frontend: The Brain: We leveraged the Perplexity API to perform live web searches. Its ability to cite real-time sources makes it the gold standard for fact-checking. The Brain: We leveraged the Perplexity API to perform live web searches. Its ability to cite real-time sources makes it the gold standard for fact-checking. The Bridge: A Flask (Python) backend handles the heavy lifting, using the YouTube Transcript API to fetch and parse dialogue data based on timestamps. The Bridge: A Flask (Python) backend handles the heavy lifting, using the YouTube Transcript API to fetch and parse dialogue data based on timestamps. The Interface: A JavaScript/HTML/CSS extension UI that injects seamlessly into the YouTube player, ensuring the data is right where your eyes are. The Interface: A JavaScript/HTML/CSS extension UI that injects seamlessly into the YouTube player, ensuring the data is right where your eyes are. Structured Parsing: We designed custom prompts to force the LLM to return structured JSON data, ensuring our UI could accurately map verdicts to the video timeline. Structured Parsing: We designed custom prompts to force the LLM to return structured JSON data, ensuring our UI could accurately map verdicts to the video timeline.

### Challenges we ran into

The "No Caption" Conundrum: Not all videos have manual or auto-generated captions. We built robust error handling and fallback states for when data is unavailable. The "No Caption" Conundrum: Not all videos have manual or auto-generated captions. We built robust error handling and fallback states for when data is unavailable. The Latency Race: Fact-checking requires a live web search, which can be slow. We optimized our processing by "chunking" transcripts so Perplexity could begin analyzing relevant sections without waiting for the whole video to load. The Latency Race: Fact-checking requires a live web search, which can be slow. We optimized our processing by "chunking" transcripts so Perplexity could begin analyzing relevant sections without waiting for the whole video to load. Cross-Context Messaging: Coordinating data between the background script (handling the API), the content script (talking to the YT player), and the popup (the UI) was a complex exercise in asynchronous logic. Cross-Context Messaging: Coordinating data between the background script (handling the API), the content script (talking to the YT player), and the popup (the UI) was a complex exercise in asynchronous logic.

### Accomplishments we're proud of

We successfully built a tool that doesn't just "summarize"—it investigates. Seeing the extension successfully debunk a common health myth in real-time and provide a link to a peer-reviewed study felt like a "eureka" moment for the future of media literacy. It was also fulfilling to see our project com to life as first-time hackers.

### What we learned

We learned that "truth" is often nuanced. We spent significant time refining our prompts to ensure the AI doesn't just label everything "False," but instead recognizes misleading contexts or partially true statements, providing a balanced perspective rather than a binary one.

### What's next

Platform Expansion: Bringing the "Voice of Reason" to TikTok, Instagram Reels, and podcasts. Platform Expansion: Bringing the "Voice of Reason" to TikTok, Instagram Reels, and podcasts. Community Integration: Allowing users to "Upvote" or "Downvote" fact-checks to create a decentralized layer of trust. Community Integration: Allowing users to "Upvote" or "Downvote" fact-checks to create a decentralized layer of trust. Mobile Support: Developing a mobile browser version or a dedicated "Share to Fact-Check" app. Mobile Support: Developing a mobile browser version or a dedicated "Share to Fact-Check" app. Multi-language Support: Breaking the language barrier to fact-check non-English content in real-time. Multi-language Support: Breaking the language barrier to fact-check non-English content in real-time.

## README (from the GitHub repository)

# Run the following commands to download the necessary dependencies
pip3 install yt-dlp youtube-transcript-api

brew install ffmpeg

pip3 install perplexityai

pip3 install flask-cors

pip3 install flask


## Detected evidence (automated analysis)

Indexed codebase: 16 recognized source files, 80 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (27 of 27)

```
.DS_Store
.env.example
.gitignore
audio_forensics.py
auditor.py
extension_folder/background.js
extension_folder/content.js
extension_folder/manifest.json
extension_folder/popup.css
extension_folder/popup.html
extension_folder/popup.html.backup
extension_folder/popup.js
extension_folder/popup.js.backup
extension_folder/REVERT_POPUP.md
extension_folder/style.css
extractor.py
main.py
README.md
requirements.txt
server.py
verdict_agent.py
vor-backend/.gitignore
vor-backend/eslint.config.mjs
vor-backend/next.config.ts
vor-backend/postcss.config.mjs
vor-backend/README.md
vor-backend/tsconfig.json
```

### Dependencies

- requirements.txt: flask, flask-cors, python-dotenv, requests, youtube-transcript-api

### Recent commits (newest first)

- Merge pull request #9 from lc0001coll/pop-up-changes
- Fixed lack of sources
- Merge pull request #8 from lc0001coll/pop-up-changes
- Server online check, check full video, and keyboard shortcut added
- Merge pull request #7 from lc0001coll/pop-up-changes
- Added timestamp and sources to claim history
- Merge Fixed server issues with the chat section of the pop up
- Fixed server issues with the chat section of the pop up
- Merge pull request #5 from lc0001coll/pop-up-changes
- Updated the pop-up with chat, history, and the vor connection
- idk
- new stuff
- dubs
- badass new update 2
- badass new update
- badass new update
- Update installation instructions in README
- Update installation instructions in README
- Merge pull request #4 from lc0001coll/pop-up-changes
- Improved server capability and improved specificity and sources of pop up

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

### extension_folder/REVERT_POPUP.md

```markdown
# Revert to previous popup UI

If you need to restore the popup to its state before the FactCheck AI redesign:

```bash
cp popup.html.backup popup.html
cp popup.js.backup popup.js
```

Then reload the extension at chrome://extensions.

```

### requirements.txt

```
flask
flask-cors
python-dotenv
requests
youtube-transcript-api

```

### main.py

```python
from youtube_transcript_api import YouTubeTranscriptApi
from perplexity import Perplexity # Ensure you have 'perplexity-py' installed

# 1. Get the Transcript
video_id = "dQw4w9WgXcQ" # Replace with your target video ID
transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
full_text = " ".join([i['text'] for i in transcript_list])

# 2. Fact Check with Perplexity Sonar
# (This model is designed to search the LIVE web)
client = Perplexity(api_key="YOUR_PERPLEXITY_API_KEY")

prompt = f"""
I am going to provide a transcript of a YouTube video. 
Identify the top 3 factual claims made and verify them using current web data.
For each claim, provide:
1. The Claim
2. Status (True/False/Misleading)
3. Evidence with a URL citation.

Transcript: {full_text[:3000]} # Clip to fit context limits
"""

response = client.chat.completions.create(
    model="sonar-pro",
    messages=[{"role": "user", "content": prompt}]
)

print(response.choices[0].message.content)
```

### server.py

```python
import os
import requests

from dotenv import load_dotenv

load_dotenv()

from flask import Flask, request, jsonify
from flask_cors import CORS

from auditor import fact_check_transcript
from extractor import get_transcript_for_time_range, get_full_transcript
from verdict_agent import get_final_verdict

app = Flask(__name__)
CORS(app)  # Allows the Chrome Extension to talk to this server

# API keys from env (set PPLX_API_KEY for Perplexity; AURIGIN_API_KEY when adding audio)
PPLX_API_KEY = os.environ.get("PPLX_API_KEY", "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")


def _perplexity_followup(question: str, context: str, api_key: str) -> str:
    """Call Perplexity to answer a followup question about the fact-check report."""
    url = "https://api.perplexity.ai/chat/completions"
    prompt = f"""You are a helpful assistant. A user asked for fact-checking on a YouTube video. Below is the fact-check report. Answer their follow-up question concisely and accurately based on this report.

FACT-CHECK REPORT:
{context[:6000]}

USER QUESTION: {question}

Provide a clear, helpful answer. If the report doesn't contain enough info, say so."""
    payload = {
        "model": "sonar-pro",
        "messages": [
            {"role": "system", "content": "You answer follow-up questions about fact-check reports. Be concise and cite the report when relevant."},
            {"role": "user", "content": prompt}
        ],
        "temperature": 0.3
    }
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    try:
        resp = requests.post(url, json=payload, headers=headers)
        resp.raise_for_status()
        return resp.json()["choices"][0]["message"]["content"]
    except Exception as e:
        return f"❌ Follow-up failed: {e}"


@app.route("/health", methods=["GET"])
def health():
    """Lightweight connectivity check for the extension."""
    return jsonify({"ok": True}), 200


def _default_audio_data() -> dict:
    """No audio file in real-time path; assume human for transcript-only audit."""
    return {"is_synthetic": False}


@app.route("/audit", methods=["POST"])
def audit():
    data = request.json or {}
    video_id = (data.get("video_id") or "").strip()
    current_time = data.get("current_time")
    check_full = data.get("check_full") is True
    text = (data.get("text") or "").strip()

    # Option 1a: Full video check
    if video_id and check_full:
        text = get_full_transcript(video_id)
        if not text:
            return jsonify({
                "verdict": "⚠️ No transcript",
                "details": "Could not fetch full transcript. Video may have no captions.",
                "risk_level": "vor-red",
            }), 200

    # Option 1b: Fetch transcript for specific timeframe when video_id + current_time provided
    elif video_id and current_time is not None:
        text = get_transcript_for_time_range(video_id, float(current_time), window_sec=45)
        if not text:
            return jsonify({
                "verdict": "⚠️ No transcript",
                "details": f"Could not fetch transcript for that timeframe. Video may have no captions.",
                "risk_level": "vor-red",
            }), 200

    if not text:
        return jsonify({
            "verdict": "⚠️ No content",
            "details": "No transcript text received. Provide video_id + current_time, or text.",
            "risk_level": "vor-red",
        }), 400

    # Clip transcript for Perplexity context limit (full video gets more context)
    max_chars = 8000 if check_full else 4000
    text_clipped = text[:max_chars]

    if not PPLX_API_KEY:
        return jsonify({
            "verdict": "⚠️ Demo mode",
            "details": "Set PPLX_API_KEY to enable live fact-checking.",
            "risk_level": "vor-red",
        }), 503

    try:
        fact_report = fact_check_transcript(text_clipped, PPLX_API_KEY)
    except Exception as e:
        return jsonify({
            "verdict": "⚠️ Verification unavailable",
            "details": f"Fact-check service error: {e}",
            "risk_level": "vor-red",
        }), 200

    # Perplexity returned an error message (e.g. rate limit, auth)
    if fact_report.strip().startswith("❌"):
        return jsonify({
            "verdict": "⚠️ Verification unavailable",
            "details": fact_report,
            "risk_level": "vor-red",
        }), 200

    audio_data = _default_audio_data()
    result = get_final_verdict(audio_data, fact_report)

    risk_level = "vor-red" if result["severity"] in ("CRITICAL", "HIGH") else "vor-green"
    details = result["message"] + "\n\n" + fact_report
    payload = {"verdict": result["verdict"], "details": details, "risk_level": risk_level}
    return jsonify(payload)


@app.route("/followup", methods=["POST"])
def followup():
    """Deep Dive Chat: answer follow-up questions about the fact-check report."""
    data = request.json or {}
    question = (data.get("question") or "").strip()
    context = (data.get("context") or "").strip()

    if not question:
        return jsonify({"error": "No question provided."}), 400

    if not context or "No prior fact-check" in context:
        return jsonify({
            "answer": "Run a fact-check first (click 'Check This Moment' on a YouTube video), then ask your question."
        }), 200

    if not PPLX_API_KEY:
        return jsonify({"error": "Set PPLX_API_KEY to enable Deep Dive Chat."}), 503

    try:
        answer = _perplexity_followup(question, context, PPLX_API_KEY)
        return jsonify({"answer": answer})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


if __name__ == "__main__":
    app.run(port=5000)

```

### audio_forensics.py

```python
import os
import requests

# 1. UPDATED ENDPOINT FOR 2026
# Aurigin now uses /api-ext/predict for external integrations
BASE_URL = "https://api.aurigin.ai/v1"
API_KEY = os.environ.get("AURIGIN_API_KEY", "ZjnWXnhHgK1U3Zo3Pzmk71EGyapGWQRZ8d8GZmG3")

def audit_audio_authenticity(file_path):
    print(f"🔬 Sending to Aurigin Forensic Cloud: {file_path}")
    
    # Ensure the file exists before sending
    if not os.path.exists(file_path):
        return {"status": "error", "message": "File not found"}

    with open(file_path, "rb") as f:
        # Aurigin 2026 expects the key 'file' and specific MIME type
        files = {
            "file": (os.path.basename(file_path), f, "audio/wav")
        }
        
        # IMPORTANT: Use 'x-api-key' in headers
        headers = {"x-api-key": API_KEY}
        
        try:
            response = requests.post(
                f"{BASE_URL}/predict", 
                headers=headers, 
                files=files,
                timeout=30 # Don't let it hang during your demo
            )

            print("Multipart response:", response.status_code, response.json())
            
            # If still 403, it's likely a tier/activation issue
            if response.status_code == 403:
                return {"status": "error", "message": "403 Forbidden: Check if your key is activated for 'api-ext'"}
                
            response.raise_for_status()
            data = response.json()
            
            # Aurigin 2026 Apollo-4 Parser
            # It provides a 'global' result and 'segments'
            global_result = data.get('global', {})
            result_label = global_result.get('result', 'human')
            confidence = global_result.get('confidence', 0)

            # In Aurigin, 'spoofed' means AI-generated/Fake
            is_synthetic = (result_label == 'spoofed')
            
            return {
                "status": "success",
                "ai_score": round(confidence * 100, 2),
                "is_synthetic": is_synthetic,
                "label": result_label
            }
            
        except Exception as e:
            return {"status": "error", "message": str(e)}

# 🧪 Run the test
if __name__ == "__main__":
    # Get the directory where THIS script is located
    base_dir = os.path.dirname(os.path.abspath(__file__))
    
    # Construct the path to the data folder relative to the script
    # Change 'xfBWk4nw440.mp3' to whatever file you just downloaded
    target_file = os.path.join(base_dir, "data", "njP23L91Jjg.mp3")
    
    print(f"DEBUG: Looking for file at: {target_file}")
    
    result = audit_audio_authenticity(target_file)
    print(result)
```

### verdict_agent.py

```python
import re


def count_claims_by_verdict(fact_report: str) -> dict:
    """
    Parse the fact-check report and count claims by VERDICT: TRUE / FALSE / MISLEADING.
    Tolerates multiple formats (VERDICT: X, **VERDICT:** X, Verdict - X, etc.).
    Returns dict with true_count, false_count, misleading_count, total, and confidence (0-100).
    Confidence = fraction of claims that are TRUE (how much we trust the claims are true).
    """
    text = (fact_report or "").strip()
    true_count = 0
    false_count = 0
    misleading_count = 0

    # Pattern 1: VERDICT: TRUE or **VERDICT:** True or Verdict: TRUE (colon or dash)
    for m in re.finditer(r"\*?\*?VERDICT\*?\*?\s*[:\-]\s*(TRUE|FALSE|MISLEADING)\b", text, re.IGNORECASE):
        label = m.group(1).upper()
        if label == "TRUE":
            true_count += 1
        elif label == "FALSE":
            false_count += 1
        elif label == "MISLEADING":
            misleading_count += 1

    # Pattern 2: "Verdict is True" / "verdict: true" (if pattern 1 found nothing)
    if true_count == 0 and false_count == 0 and misleading_count == 0:
        for m in re.finditer(r"(?:verdict|VERDICT)\s*(?:is|:|\-)\s*(true|false|misleading)\b", text, re.IGNORECASE):
            label = m.group(1).upper()
            if label == "TRUE":
                true_count += 1
            elif label == "FALSE":
                false_count += 1
            elif label == "MISLEADING":
                misleading_count += 1

    total = true_count + false_count + misleading_count
    confidence = round((true_count / total * 100), 0) if total else None
    return {
        "true_count": true_count,
        "false_count": false_count,
        "misleading_count": misleading_count,
        "total": total,
        "confidence": int(confidence) if confidence is not None else None,
    }


def get_final_verdict(audio_data: dict, fact_report: str) -> dict:
    """
    Combines audio forensics and fact-check into a single verdict.
    Separates simple AI use from malicious disinformation.
    """
    fact_report = fact_report or ""

    # CASE 1: The Malicious Deepfake
    if audio_data['is_synthetic'] and "False" in fact_report:
        return {
            "verdict": "🚨 MALICIOUS DEEPFAKE",
            "severity": "CRITICAL",
            "message": "Both voice forensics and factual audit failed. This is a weaponized clone."
        }
    
    # CASE 2: The AI Parody / Assistant
    elif audio_data['is_synthetic'] and "True" in fact_report:
        return {
            "verdict": "⚠️ AI-GENERATED (ACCURATE)",
            "severity": "MEDIUM",
            "message": "The voice is synthetic, but the claims are factually correct. Likely a parody or AI-read news."
        }

    # CASE 3: Human Lying
    elif not audio_data['is_synthetic'] and "False" in fact_report:
        return {
            "verdict": "🤥 HUMAN MISINFORMATION",
            "severity": "HIGH",
            "message": "The voice is human, but the claims are false. Traditional misinformation."
        }

    return {"verdict": "✅ AUTHENTIC", "severity": "LOW", "message": "Content is human and factual."}
```

### auditor.py

```python
import os
import requests
import re

def get_video_id(url):
    pattern = r'(?:v=|\/)([0-9A-Za-z_-]{11}).*'
    match = re.search(pattern, url)
    return match.group(1) if match else None

def fact_check_transcript(transcript_text, api_key):
    print(f"🧠 Analyzing transcript ({len(transcript_text)} chars) with Perplexity Sonar...")
    
    url = "https://api.perplexity.ai/chat/completions"
    
    prompt = f"""
You are a professional forensic fact-checker. Analyze the following YouTube transcript segment.
The transcript includes [M:SS] markers showing when each phrase was spoken (e.g. [1:23] = 1 min 23 sec).

For EACH significant factual claim (extract up to 5):

1. **CLAIM:** Paraphrase the claim concisely in quotes—use clear, neutral language.
2. **TIMESTAMP:** Use the [M:SS] marker from the transcript where this claim appears (e.g. TIMESTAMP: 0:45 or TIMESTAMP: 1:23).
3. **VERDICT:** TRUE / FALSE / MISLEADING
4. **WHY (if FALSE or MISLEADING):** Explain specifically why the claim is false or misleading. Cite contradictory evidence.
5. **SOURCES:** List 1–3 full URLs that support your verdict. Use real, clickable links (https://...).

Format each claim exactly like this:
---
CLAIM: "[paraphrased claim]"
TIMESTAMP: 0:45
VERDICT: TRUE
WHY: [explanation, keep it short if TRUE]
SOURCES:
- [URL 1]
- [URL 2]
---
For VERDICT use exactly one of: VERDICT: TRUE, VERDICT: FALSE, or VERDICT: MISLEADING on its own line.
For TIMESTAMP use the [M:SS] value from the transcript (required for parsing).
Try to avoid covering the same claims in a single fact check.
Try to avoid covering personal claims that are anecdotal and not used as evidence for incorrect claims.

Transcript:
{transcript_text}
"""

    payload = {
        "model": "sonar-pro",
        "messages": [
            {"role": "system", "content": "You are a forensic fact-checker. Search the live web. Always provide exact claim, verdict (TRUE/FALSE/MISLEADING), explanation when false, and real source URLs."},
            {"role": "user", "content": prompt}
        ],
        "temperature": 0.2
    }
    
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }

    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        result = response.json()
        return result['choices'][0]['message']['content']
    except Exception as e:
        return f"❌ Fact Check Failed: {e}"

if __name__ == "__main__":
    # 1. Setup
    MY_API_KEY = "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" # <--- PUT YOUR KEY HERE
    
    # 2. Identify the file
    target_url = input("🔗 Paste the YouTube URL you just processed: ")
    vid = get_video_id(target_url)
    
    if not vid:
        print("Invalid URL.")
        exit()

    file_path = f"data/{vid}_transcript.txt"
    
    # 3. Run the audit
    if os.path.exists(file_path):
        with open(file_path, "r") as f:
            text = f.read()
        
        report = fact_check_transcript(text, MY_API_KEY)
        print("\n--- FINAL FORENSIC REPORT ---")
        print(report)
    else:
        print(f"❌ Error: Could not find {file_path}. Did you run extractor.py for this URL first?")
```

### extractor.py

```python
import os
import re
from youtube_transcript_api import YouTubeTranscriptApi
import yt_dlp

def get_video_id(url):
    """Extracts the video ID from a standard or short YouTube URL."""
    pattern = r'(?:v=|\/)([0-9A-Za-z_-]{11}).*'
    match = re.search(pattern, url)
    return match.group(1) if match else None

def _format_timestamp(seconds: float) -> str:
    """Convert seconds to [M:SS] format for transcript markers."""
    m = int(seconds // 60)
    s = int(seconds % 60)
    return f"[{m}:{s:02d}]"


def get_transcript_for_time_range(video_id: str, center_sec: float, window_sec: float = 30):
    """Fetch transcript and return only segments within [center - window/2, center + window/2] seconds.
    Each segment is prefixed with [M:SS] so the auditor can reference timestamps for claims."""
    start_sec = max(0, center_sec - window_sec / 2)
    end_sec = center_sec + window_sec / 2

    try:
        try:
            transcript_data = YouTubeTranscriptApi.get_transcript(video_id)
        except Exception:
            api = YouTubeTranscriptApi()
            transcript_data = api.fetch(video_id)

        text_parts = []
        for entry in transcript_data:
            if isinstance(entry, dict):
                seg_start = entry.get("start", 0)
                seg_duration = entry.get("duration", 0)
                seg_text = entry.get("text", "")
            else:
                seg_start = getattr(entry, "start", 0)
                seg_duration = getattr(entry, "duration", 0)
                seg_text = getattr(entry, "text", "")

            seg_end = seg_start + seg_duration
            if seg_end >= start_sec and seg_start <= end_sec:
                text_parts.append(_format_timestamp(seg_start) + " " + seg_text)

        return " ".join(text_parts).strip() or None
    except Exception as e:
        print(f"❌ Transcript fetch error: {e}")
        return None


def get_full_transcript(video_id: str):
    """Fetch full transcript with [M:SS] markers for each segment."""
    try:
        try:
            transcript_data = YouTubeTranscriptApi.get_transcript(video_id)
        except Exception:
            api = YouTubeTranscriptApi()
            transcript_data = api.fetch(video_id)

        text_parts = []
        for entry in transcript_data:
            if isinstance(entry, dict):
                seg_start = entry.get("start", 0)
                seg_text = entry.get("text", "")
            else:
                seg_start = getattr(entry, "start", 0)
                seg_text = getattr(entry, "text", "")
            text_parts.append(_format_timestamp(seg_start) + " " + seg_text)

        return " ".join(text_parts).strip() or None
    except Exception as e:
        print(f"❌ Transcript fetch error: {e}")
        return None


def download_transcript(video_id, output_dir="data"):
    """Fetches the transcript and handles both dicts and objects."""
    try:
        from youtube_transcript_api import YouTubeTranscriptApi
        
        # Try fetching the transcript
        # If the static method fails, we use the instance method
        try:
            transcript_data = YouTubeTranscriptApi.get_transcript(video_id)
        except:
            api = YouTubeTranscriptApi()
            transcript_data = api.fetch(video_id)
            
        # Universal handler: Check if entry is a dict or an object
        text_parts = []
        for entry in transcript_data:
            if isinstance(entry, dict):
                text_parts.append(entry.get('text', ''))
            else:
                # This handles the 'FetchedTranscriptSnippet' object
                text_parts.append(getattr(entry, 'text', ''))
        
        full_text = " ".join(text_parts).strip()
        
        if not full_text:
            raise ValueError("Transcript is empty or could not be parsed.")

        filename = os.path.join(output_dir, f"{video_id}_transcript.txt")
        with open(filename, "w", encoding="utf-8") as f:
            f.write(full_text)
        
        print(f"✅ Transcript saved to: {filename}")
        return full_text
    except Exception as e:
        print(f"❌ Transcript Error: {e}")
        return None

def download_audio(url, video_id, output_dir="data"):
    """Downloads audio and converts it to MP3 using yt-dlp."""
    output_path = os.path.join(output_dir, f"{video_id}.mp3")
    
    ydl_opts = {
        'format': 'bestaudio/best',
        'outtmpl': os.path.join(output_dir, f"{video_id}.%(ext)s"),
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'mp3',
            'preferredquality': '192',
        }],
        'quiet': True,
    }

    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            ydl.download([url])
        print(f"✅ Audio saved to: {output_path}")
        return output_path
    except Exception as e:
        print(f"❌ Audio Download Error: {e}")
        return None

if __name__ == "__main__":
    # 1. Input Configuration
    target_url = input("🔗 Paste YouTube URL: ")
    vid = get_video_id(target_url)
    
    if not vid:
        print("Invalid URL.")
        exit()

    # Create data folder if it doesn't exist
    os.makedirs("data", exist_ok=True)

    print(f"🚀 Processing Video ID: {vid}...")
    
    # 2. Run Pipeline
    text = download_transcript(vid)
    audio = download_audio(target_url, vid)

    if text and audio:
        print("\n✨ Phase 1 Complete! You now have the raw text and audio file.")
```

### vor-backend/next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
};

export default nextConfig;

```

### extension_folder/style.css

```css
#vor-fact-check-overlay {
  position: fixed;
  top: 80px;
  right: 20px;
  width: 280px;
  padding: 15px;
  background: rgba(0, 0, 0, 0.85);
  color: white;
  border-radius: 8px;
  z-index: 99999;
  font-family: sans-serif;
  border: 1px solid #444;
  transition: all 0.3s ease;
}

.vor-red { border-left: 5px solid #ff4d4d !important; }
.vor-green { border-left: 5px solid #2ecc71 !important; }
```

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