# Project export: Sauce, please? A Background Check for Every Video You Watch

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: Our AI agents fact-check short-form videos in real time: detect deepfakes, verify claims, add missing context, and cite sources. WSJ-style investigative journalists for every short-form video.
- Devpost: https://devpost.com/software/sauce-please
- GitHub: https://github.com/GabeTsai/short-detective
- Video: https://www.youtube.com/embed/5b_N8TrLRHs?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Peter Zhang (14 commits)

## Devpost submission (written by the team)

### Inspiration

Short-form videos (TikTok, Instagram Reels, YouTube Shorts) have become the default way people absorb “news,” but it’s optimized for speed + emotion, not truth. With deepfakes, voice clones, and out-of-context clips, misinformation is spreading faster than ever. A careful and intentional human could do the right thing: Google the claim, check the channel, find the original clip, and compare sources. But almost nobody has the time, energy, or incentive to do that for every video they scroll past. Investigative journalists do this work professionally, but they have limited bandwidth and can’t cover everything that goes viral. This constraint can be overcome with AI. So we built Sauce, please to make that investigative workflow automatic. By simulating a rigorous investigative newsroom with a team of specialized agents, Sauce, please aims to deliver WSJ-level verification in real time for every short-form video you watch.

### What it does

Sauce, please is a Chrome extension that adds a real-time “investigative report” sidebar to short-form videos (currently YouTube Shorts, designed to expand to TikTok and Instagram Reels). Every time you open a Short, Sauce automatically generates a report with two parts: 1) Video Risk: Is the video itself manipulated? We check whether the video artifact is a truthful representation of what happened. Detects signals of AI/deepfake / synthetic audio and other manipulation Flags clipping/edits that may indicate selectively presented footage Reports confidence (low/medium/high) and what signals we used We avoid overclaiming: if it’s a real person speaking, we don’t call it “fake”. We only report manipulation risk signals. 2) Context Risk: Even if real, is it misleading? We verify whether the story being told is accurate. Extracts the video’s key claims (what viewers are likely to believe) Checks whether claims are false, missing key context, or misrepresent who/what/when/where Provides claim -> evidence -> sources, so users can inspect the receipts and decide At the top, Sauce shows a simple summary: No Risk / Video Risk / Context Risk / Both. Sauce will make a clear claim only when it can back it up. Every conclusion comes with evidence and citations that the users can read themself, and they're free to disagree or reach a different interpretation. We’re not here to push a viewpoint. We’re here to surface receipts and context so the user can make their own judgment.

### How we built it

We built Sauce as a FastAPI-based analysis pipeline designed to feel like a WSJ-style investigative newsroom, except the “reporters” are specialized AI agents working in parallel. Instead of a single model guessing, we split the job into roles (reporters, researchers, background-checkers) and then use an editor agent to synthesize an evidence-backed report. Backend flow (end-to-end) 1) Frontend sends a batch of Short URLs to the backend. 2) For each URL, we check the cache and skip anything we’ve already processed. 3) We download a lightweight clip (first 30 seconds, 144p, with audio) to minimize cost and latency. 4) For each video, we run three investigations in parallel: Transcription (audio to text): Modal + vLLM (GPU-backed) Channel background check: scrape channel metadata + recent uploads, then assess credibility Multimodal video analysis: Gemini analyzes visual content and manipulation/propaganda signals 5) An Editor-in-Chief agent (GPT synthesis) merges all evidence into a structured report: Mismatch level Video Risk (AI/manipulation confidence + why) Context Risk (claims + evidence + sources) Presentation Risk (framing/propaganda techniques, when relevant) 6) Results are stored in cache.json, so repeated videos can return instantly. Engineering for speed Short-form video is fast, so the backend is built for throughput: Parallel downloads (threaded) Parallel analysis across many videos at once Per-video parallel subtasks (transcription + channel + semantic analysis) Caching to avoid re-processing the same viral clips This is how Sauce can keep up with scroll speed: we reduce input size, parallelize aggressively, and treat evidence-gathering like a newsroom pipeline.

### Challenges we ran into

Latency vs. depth: Real verification takes time, but Shorts move fast. We had to engineer a pipeline that stays thorough while still feeling immediate to the user. Separating “fake” vs. “misleading”: Many videos are real but framed deceptively. That’s why we split the report into Video Risk (manipulation/deepfake signals) and Context Risk (claims + missing context). Avoiding hallucinated certainty: A credibility tool can’t confidently “declare truth” without receipts. We built the system to cite evidence and to output Uncertain when the proof is weak. Self-hosting - There were a lot of hacks and knobs and dials I had to experiment with to get some configs and setups to work, but it was super interesting to see how Modal abstracted away a lot of the tediousness of self-hosting - Gabe

### Accomplishments we're proud of

A fully functional, shippable product: Sauce is a shippable Chrome extension with a real-time sidebar experience, not just a concept demo. A fully functional, shippable product: Sauce is a shippable Chrome extension with a real-time sidebar experience, not just a concept demo. WSJ-style “newsroom” architecture: We simulated an investigative journalist workflow using a team of specialized agents (transcription, channel background checks, multimodal video analysis) plus an editor agent that synthesizes an evidence-backed report. WSJ-style “newsroom” architecture: We simulated an investigative journalist workflow using a team of specialized agents (transcription, channel background checks, multimodal video analysis) plus an editor agent that synthesizes an evidence-backed report. Evidence-first outputs: We don’t just label content: we provide claim -> evidence -> sources, so users can verify and reach their own conclusions. We want to earn the trust of our users. Evidence-first outputs: We don’t just label content: we provide claim -> evidence -> sources, so users can verify and reach their own conclusions. We want to earn the trust of our users.

### What we learned

Trust requires receipts. Users don’t trust black-box “true/false” labels. They trust sources, evidence, and a clear chain of reasoning. Trust requires receipts. Users don’t trust black-box “true/false” labels. They trust sources, evidence, and a clear chain of reasoning. Every moment matters. Users don't want to wait 2 minutes for facts. We prefetch and cache aggressively to create a seamless user experience. Every moment matters. Users don't want to wait 2 minutes for facts. We prefetch and cache aggressively to create a seamless user experience. “Uncertain” is a feature, not a failure. In verification, refusing to guess is often the most responsible output—and it preserves credibility. “Uncertain” is a feature, not a failure. In verification, refusing to guess is often the most responsible output—and it preserves credibility. Most misinformation isn’t “fake,” it’s framed. The biggest problem we saw was real clips used with missing or distorted context, which is why separating Video Risk from Context Risk matters. Most misinformation isn’t “fake,” it’s framed. The biggest problem we saw was real clips used with missing or distorted context, which is why separating Video Risk from Context Risk matters. Self hosting with Modal - I learned a ton about Modal's serverless self-hosting ecosystem, and how to integrate it with VLLM and HuggingFace Transformers - Gabe Self hosting with Modal - I learned a ton about Modal's serverless self-hosting ecosystem, and how to integrate it with VLLM and HuggingFace Transformers - Gabe

### What's next

for Sauce, please? Sauce is already a fully functional, shippable Chrome extension. Next, we want to turn it into the default trust layer for short-form video: Ship v1 publicly: publish to the Chrome Web Store with onboarding and a waitlist for early users. Expand beyond YouTube Shorts: bring the same Video Risk / Context Risk report to TikTok and Instagram Reels. Make it faster and more real-time: push streaming progress + partial results to the sidebar (the backend already supports chunked outputs). Improve evidence quality: stronger source ranking, better linking to original footage, and clearer “what would change our mind” explanations. Go multilingual: support verification across languages so users aren’t limited to English-only sources. Close the feedback loop: allow users to flag incorrect analysis and submit better sources, improving accuracy over time. Long term, our goal is simple: the truth layer for every short-form video.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 34 recognized source files, 204 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- React (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (43 of 43)

```
.DS_Store
.env.example
.gitignore
backend/__init__.py
backend/.DS_Store
backend/config.py
backend/semantic_analysis.py
backend/server/.DS_Store
backend/server/channel_scraper.py
backend/server/config.py
backend/server/download_video.py
backend/server/dummy_server.py
backend/server/extract_audio.py
backend/server/nonexistent.py
backend/server/README.md
backend/server/semantic_analysis_real.py
backend/server/server.py
backend/server/summarize_videos.py
backend/server/voice_to_text_real.py
backend/server/web_search_real.py
backend/Untitled
backend/utils/__init__.py
backend/utils/llm_caller.py
backend/utils/test_llm_caller.py
backend/voice_to_text.py
backend/web_search.py
frontend/background.js
frontend/content.js
frontend/icons/README.md
frontend/manifest.json
frontend/popup.css
frontend/popup.html
frontend/popup.js
frontend/README.md
frontend/YT-SHORTS-SCROLLER/background.js
frontend/YT-SHORTS-SCROLLER/content.js
frontend/YT-SHORTS-SCROLLER/manifest.json
frontend/YT-SHORTS-SCROLLER/ui.css
requirements.txt
tests/test_continuous_batching.py
tests/test_semantic_analysis.py
tests/test_voice_to_text.py
tests/test_web_search.py
```

### Dependencies

- requirements.txt: aiohappyeyeballs@==2.6.1, aiohttp@==3.13.3, aiosignal@==1.4.0, annotated-doc@==0.0.4, annotated-types@==0.7.0, anthropic@==0.79.0, anyio@==4.12.1, async-timeout@==5.0.1, attrs@==25.4.0, cbor2@==5.8.0, certifi@==2026.1.4, cffi@==2.0.0, charset-normalizer@==3.4.4, click@==8.3.1, cryptography@==46.0.5, distro@==1.9.0, docstring_parser@==0.17.0, fastapi@==0.115.6, frozenlist@==1.8.0, google-ai-generativelanguage@==0.6.15, google-api-core@==2.29.0, google-api-python-client@==2.190.0, google-auth@==2.49.0.dev0, google-auth-httplib2@==0.3.0, google-genai@==1.63.0, googleapis-common-protos@==1.72.0, grpcio@==1.78.0, grpcio-status@==1.71.2, grpclib@==0.4.9, h11@==0.16.0, h2@==4.3.0, hpack@==4.1.0, httpcore@==1.0.9, httplib2@==0.31.2, httptools@==0.7.1, httpx@==0.28.1, hyperframe@==6.1.0, idna@==3.11, jiter@==0.13.0, jsonschema@==4.26.0, jsonschema-specifications@==2025.9.1, markdown-it-py@==4.0.0, mdurl@==0.1.2, mistral_common@==1.9.1, modal@==1.3.3, multidict@==6.7.1, nodejs-wheel-binaries@==24.13.1, numpy@==2.4.2, openai@==2.21.0, pillow@==12.1.1, propcache@==0.4.1, proto-plus@==1.27.1, protobuf@==5.29.6, pyasn1@==0.6.2, pyasn1_modules@==0.4.2, pycountry@==24.6.1, pycparser@==3.0, pydantic@==2.12.5, pydantic_core@==2.41.5, pydantic-extra-types@==2.11.0, Pygments@==2.19.2, pyparsing@==3.3.2, pytest, python-dotenv@==1.2.1, pytubefix@==10.3.6, PyYAML@==6.0.3, referencing@==0.37.0, regex@==2026.1.15, requests@==2.32.5, rich@==14.3.2, rpds-py@==0.30.0, shellingham@==1.5.4, sniffio@==1.3.1, soundfile@==0.13.1, soxr@==1.0.0, starlette@==0.41.3, synchronicity@==0.11.1, tenacity@==9.1.4, tiktoken@==0.12.0, toml@==0.10.2, tqdm@==4.67.3, typer@==0.23.1, types-certifi@==2021.10.8.3, types-toml@==0.10.8.20240310, typing_extensions@==4.15.0, typing-inspection@==0.4.2, uritemplate@==4.2.0, urllib3@==2.6.3, uvicorn@==0.34.0, uvloop@==0.22.1, watchfiles@==1.1.1, websockets@==15.0.1, yarl@==1.22.0, yt-dlp@==2026.2.4

### Recent commits (newest first)

- Merge branch 'main' of https://github.com/GabeTsai/short-detective
- updated google gemini api to use genai; updated propts in config for perplexity and edited for robust handling of transcription errors; appended mini channel scraperfunction to semantic analysis to enrich analysis; fixed race condition in download_videos.py where videos were analyzed with channel context from other videos by returning URLs in original URL order
- Merge remote-tracking branch 'refs/remotes/origin/main'
- edited frontend descriptions
- MUTE
- Merge remote-tracking branch 'refs/remotes/origin/main'
- praying
- stash version
- hyperlink
- changes
- updates
- integrated perplexity
- added graph
- fixed server bugs
- temp staging
- Merge branch 'main' of https://github.com/GabeTsai/short-detective
- perplexity web search integration
- frontend MVP DONE
- fixed merge
- updated stuff

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

### requirements.txt

```
aiohappyeyeballs==2.6.1
aiohttp==3.13.3
aiosignal==1.4.0
annotated-doc==0.0.4
annotated-types==0.7.0
anthropic==0.79.0
anyio==4.12.1
async-timeout==5.0.1
attrs==25.4.0
cbor2==5.8.0
certifi==2026.1.4
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
cryptography==46.0.5
distro==1.9.0
docstring_parser==0.17.0
fastapi==0.115.6
frozenlist==1.8.0
google-ai-generativelanguage==0.6.15
google-api-core==2.29.0
google-api-python-client==2.190.0
google-auth==2.49.0.dev0
google-auth-httplib2==0.3.0
google-genai==1.63.0
googleapis-common-protos==1.72.0
grpcio==1.78.0
grpcio-status==1.71.2
grpclib==0.4.9
h11==0.16.0
h2==4.3.0
hpack==4.1.0
httpcore==1.0.9
httplib2==0.31.2
httptools==0.7.1
httpx==0.28.1
hyperframe==6.1.0
idna==3.11
jiter==0.13.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
markdown-it-py==4.0.0
mdurl==0.1.2
mistral_common==1.9.1
modal==1.3.3
multidict==6.7.1
nodejs-wheel-binaries==24.13.1
numpy==2.4.2
openai==2.21.0
pillow==12.1.1
propcache==0.4.1
proto-plus==1.27.1
protobuf==5.29.6
pyasn1==0.6.2
pyasn1_modules==0.4.2
pycountry==24.6.1
pycparser==3.0
pydantic==2.12.5
pydantic-extra-types==2.11.0
pydantic_core==2.41.5
Pygments==2.19.2
pyparsing==3.3.2
python-dotenv==1.2.1
pytubefix==10.3.6
PyYAML==6.0.3
referencing==0.37.0
regex==2026.1.15
requests==2.32.5
rich==14.3.2
rpds-py==0.30.0
shellingham==1.5.4
sniffio==1.3.1
soundfile==0.13.1
soxr==1.0.0
starlette==0.41.3
synchronicity==0.11.1
tenacity==9.1.4
tiktoken==0.12.0
toml==0.10.2
tqdm==4.67.3
typer==0.23.1
types-certifi==2021.10.8.3
types-toml==0.10.8.20240310
typing-inspection==0.4.2
typing_extensions==4.15.0
uritemplate==4.2.0
urllib3==2.6.3
uvicorn==0.34.0
uvloop==0.22.1
watchfiles==1.1.1
websockets==15.0.1
yarl==1.22.0
yt-dlp==2026.2.4
pytest


```

### backend/server/server.py

```python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from download_video import download_videos_batch, video_id_from_url
from summarize_videos import summarize_videos
import argparse
import json
import os
from fastapi.responses import StreamingResponse
import time
from fastapi import FastAPI, Body
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import time

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

global USE_CACHE
USE_CACHE = False

global STORAGE_DICT
STORAGE_DICT = {}

@app.post("/send_urls", status_code=204)
def send_urls(raw_urls: list[str] = Body(...)):
    print(raw_urls)
    uncached_urls = []
    if USE_CACHE:
        try:
            with open("cache.json", "r") as f:
                cache = json.load(f)
                print(cache.keys())
        except Exception as e:
            print(f"Error loading cache: {e}")
            cache = {}
    else:
        raise
        cache = {}
    for raw_url in raw_urls:
        video_id = video_id_from_url(raw_url)

        if os.path.join("videos", video_id) not in cache.keys():
            uncached_urls.append(raw_url)
    paths = download_videos_batch(uncached_urls)
    summary_inputs = [(path, url) for path, url in zip(paths, uncached_urls)]
    summaries = summarize_videos(summary_inputs, STORAGE_DICT)
    for path in summaries.keys():
        # path is "VIDEO_ID.mp4", extract video_id
        video_id = path.removesuffix(".mp4")
        cache[video_id] = summaries[path]
    if USE_CACHE:
        with open("cache.json", "w") as f:
            json.dump(cache, f, indent=2)
    else:
        print(cache)


@app.get("/")
def root():
    """Root endpoint."""
    return {"message": "Short Detective API"}


@app.get("/get-info")
def get_info(url: str):
    """Get cached info for a video URL."""
    video_id = video_id_from_url(url)
    video_id = os.path.join("videos", video_id)
    try:
        with open("cache.json", "r") as f:
            cache = json.load(f)
    except FileNotFoundError:
        return {"message": "Loading..."}
    print(video_id)
    print(cache.keys())
    if video_id in cache:
        print("CACHE HIT")
        return {"message": cache[video_id]}
    else:
        print("CACHE MISS")
        return {"message": f"Loading..."}

    return {"message": "Loading...", "is_streaming": True}


def hi_stream():
    for i in range(10):
        yield "hi{i}\n"
        time.sleep(0.5)  # optional delay to show streaming


@app.get("/stream")
def stream(url: str):
    return StreamingResponse(hi_stream(), media_type="text/plain")

if __name__ == "__main__":
    import uvicorn
    parser = argparse.ArgumentParser()
    parser.add_argument("--cache", action="store_true", help="Enable caching")
    args = parser.parse_args()
    
    USE_CACHE = True
    uvicorn.run(app, host="0.0.0.0", port=8080)

```

### frontend/popup.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Short Detective</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="popup">
    <header class="popup-header">
      <h1>Short Detective</h1>
    </header>
    <main class="popup-content">
      <p class="description">Detect and analyze the YouTube Shorts in your feed.</p>
      <div id="result" class="result" hidden></div>
    </main>
    <footer class="popup-footer">
      <button id="start" type="button" class="btn btn-primary">Start</button>
    </footer>
  </div>
  <script src="popup.js"></script>
</body>
</html>

```

### backend/__init__.py

```python
"""Backend package for short-detective.

Example usage:
    from backend import transcribe
    
    # Easy way: automatically uses TRANSCRIPTION_URL from .env
    text = transcribe("audio.mp3")
    
    # With specific language
    text = transcribe("audio.mp3", language="es")
    
    # Advanced usage with explicit URL
    from backend import voice_to_text, config
    url = config.get_transcription_url()
    text = voice_to_text("audio.mp3", url, language="en")
"""

# Import config module for easy access
from backend import config

# Import voice-to-text functionality
from backend.voice_to_text import (
    voice_to_text,
    transcribe,
    clear_client_cache,
    app,
    serve,
)

# Define public API
__all__ = [
    "config",
    "transcribe",           # Convenience function (recommended)
    "transcribe_batch",     # Batch processing with concurrent requests
    "voice_to_text",        # Advanced usage
    "clear_client_cache",   # Clear cached clients (for debugging)
    "app",
    "serve",
]

```

### frontend/popup.css

```css
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 14px;
  color: #1a1a1a;
  min-width: 320px;
  max-width: 360px;
}

.popup {
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.popup-header {
  border-bottom: 1px solid #e0e0e0;
  padding-bottom: 8px;
}

.popup-header h1 {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
}

.popup-content {
  flex: 1;
  min-height: 60px;
}

.popup-content .description {
  margin: 0 0 12px;
  color: #555;
}

.result {
  padding: 8px 12px;
  background: #f5f5f5;
  border-radius: 6px;
  font-size: 13px;
}

.popup-footer {
  border-top: 1px solid #e0e0e0;
  padding-top: 12px;
}

.btn {
  width: 100%;
  padding: 10px 16px;
  font-size: 14px;
  font-weight: 500;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background-color 0.15s;
}

.btn-primary {
  background: #2563eb;
  color: white;
}

.btn-primary:hover {
  background: #1d4ed8;
}

.btn-primary:active {
  background: #1e40af;
}

.btn-primary:disabled {
  background: #93b4f5;
  cursor: not-allowed;
}

.result.error {
  background: #fee2e2;
  color: #991b1b;
}

.result.success {
  background: #dcfce7;
  color: #166534;
}

```

### tests/test_web_search.py

```python
"""
Tests for web search fact-checking functionality.
"""

import pytest
import os
from backend.web_search import search_web_from_transcript, format_search_results
import time


def test_search_web_sample():
    """Sample test of web search with example oats transcript."""
    # Skip if no API key
    if not os.environ.get("PERPLEXITY_API_KEY"):
        pytest.skip("PERPLEXITY_API_KEY not set - add to .env file")
    
    # Example transcript with misleading health claims
    transcript = """Your breakfast versus my breakfast. Your breakfast starts with oatmeal. 
    Oats are a grain. Grains are seeds. Seeds are highly defended. They are full of plant 
    defense chemicals. In the case of oats, oats are full of phytic acid, a substance 
    that chelates, that bites minerals and prevents their absorption. Oats are also full 
    of digestive enzyme inhibitors. Oats are total bullshit."""
    
    print("\n" + "="*80)
    print("Testing Web Search Fact-Checking")
    print("="*80)
    print(f"\nTranscript: {transcript[:100]}...")
    print("\nSearching for fact-check sources...\n")
    
    # Perform web search
    start = time.time()
    response = search_web_from_transcript(transcript, max_results=5)
    
    # Basic validation
    assert response is not None
    assert len(response.results) > 0
    assert response.summary is not None
    
    # Display results
    print(format_search_results(response))
    print("\n✓ Web search completed successfully")
    end = time.time()
    print(f"Time taken: {end - start} seconds")

if __name__ == "__main__":
    test_search_web_sample()
```

### tests/test_voice_to_text.py

```python
from backend import voice_to_text, config
import urllib.request
import json
import time

def test_voice_to_text():
    url = config.get_transcription_url()
    
    print(f"Testing transcription with modal server URL: {url}")
    
    # Check if server is responsive
    print("Checking server health...")
    try:
        urllib.request.urlopen(f"{url}/health", timeout=5)
        print("✓ Server is responding")
    except Exception as e:
        print(f"✗ Server health check failed: {e}")
        print("Server needs cold start (~2-5 min)")
    
    # Check if models are loaded
    print("Checking if model is loaded...")
    max_retries = 0
    for i in range(max_retries):
        try:
            response = urllib.request.urlopen(f"{url}/v1/models", timeout=10)
            data = json.loads(response.read())
            if data.get("data") and len(data["data"]) > 0:
                print(f"✓ Model loaded: {data['data'][0]['id']}")
                break
            else:
                print(f"  Model not ready yet (attempt {i+1}/{max_retries})...")
        except Exception as e:
            print(f"  Model check failed (attempt {i+1}/{max_retries}): {e}")
        
        if i < max_retries - 1:
            time.sleep(10)
    
    # Perform transcription
    print("\nStarting transcription...")
    for i in range(8):
        text = voice_to_text("test_data/test_audio_0.mp3", url)
    print(f"\n--- Transcript ({len(text)} characters) ---")
    print(text)
    print("---\n")
    
    assert text is not None
    assert len(text) > 0
    print("✓ Test passed!")

if __name__ == "__main__":
    test_voice_to_text()
```

### tests/test_semantic_analysis.py

```python
from backend.semantic_analysis import analyze_video, analyze_video_quick
from dotenv import load_dotenv
import os
import sys

# Load .env from project root
load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env'))

def test_semantic_analysis():
    # Get API key from environment
    api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
    
    if not api_key:
        print("✗ GEMINI_API_KEY or GOOGLE_API_KEY not found in environment")
        print("Please set one of these environment variables to run the test")
        return
    
    print(f"Testing semantic video analysis with Gemini API...")
    
    # Use a test video - update this path to your test video
    video_path = "test_data/oatsarebadguy.mp4"
    
    if len(sys.argv) > 1:
        video_path = sys.argv[1]
    
    if not os.path.exists(video_path):
        print(f"✗ Test video not found at: {video_path}")
        print("Please provide a valid video path as argument or create test_data/test_video.mp4")
        return
    
    print(f"Using video: {video_path}")
    
    # Perform analysis
    print("\nStarting video analysis...")
    print("(This may take 30-60 seconds depending on video length)")
    analysis = analyze_video(video_path, api_key=api_key)
    
    print("\n" + "="*80)
    print(analysis)
    print("="*80 + "\n")
    
    assert analysis is not None
    assert len(analysis) > 0
    assert "VIDEO CONTENT ANALYSIS REPORT" in analysis
    print("✓ Test passed!")

if __name__ == "__main__":
    from google import genai
    client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
    test_semantic_analysis()

```

### frontend/popup.js

```javascript
(function () {
  'use strict';

  const startBtn = document.getElementById('start');
  const resultEl = document.getElementById('result');

  function showResult(text, type = 'info') {
    resultEl.textContent = text;
    resultEl.hidden = false;
    resultEl.classList.remove('error', 'success');
    if (type === 'error') resultEl.classList.add('error');
    if (type === 'success') resultEl.classList.add('success');
  }

  function hideResult() {
    resultEl.hidden = true;
    resultEl.classList.remove('error', 'success');
  }

  function setLoading(loading) {
    startBtn.disabled = loading;
    startBtn.textContent = loading ? 'Working...' : 'Start';
  }

  startBtn.addEventListener('click', async () => {
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

      if (!tab?.url) {
        showResult('No active tab or URL.', 'error');
        return;
      }

      if (!tab.url.includes('youtube.com/shorts')) {
        showResult('Please navigate to a YouTube Shorts page first.', 'error');
        return;
      }

      hideResult();
      setLoading(true);
      showResult('Extracting Shorts URLs...');

      const response = await chrome.tabs.sendMessage(tab.id, { type: 'START' });

      if (response?.ok) {
        const count = response.urlCount || 0;
        showResult(`Done! Found and sent ${count} Shorts URL${count !== 1 ? 's' : ''}.`, 'success');
      } else {
        showResult(response?.error || 'Something went wrong.', 'error');
      }
    } catch (err) {
      // Better error message for "receiving end does not exist"
      if (err.message?.includes('Receiving end does not exist')) {
        showResult('Content script not loaded. Try refreshing the YouTube page.', 'error');
      } else {
        showResult(err.message || 'Something went wrong.', 'error');
      }
    } finally {
      setLoading(false);
    }
  });
})();

```

### backend/semantic_analysis.py

```python
"""
Semantic video analysis using Google Gemini API.
Provides detailed analysis of video content with focus on detecting misinformation,
propaganda, and agenda-pushing content.
"""

import os
import time
from pathlib import Path
from typing import Optional
import time

import google.generativeai as genai

from .config import (
    GEMINI_MODEL_VIDEO,
    SEMANTIC_ANALYSIS_PROMPT,
    SEMANTIC_ANALYSIS_QUICK_PROMPT,
)

def get_formatted_result(analysis: str, video_file: Path, video_path: str, model_name: str) -> str:
    return f"""
{'='*80}
VIDEO CONTENT ANALYSIS REPORT
{'='*80}
File: {video_file.name}
Path: {video_path}
Model: {model_name}
{'='*80}

{analysis}

{'='*80}
End of Analysis
"""

def analyze_video(
    video_path: str,
    api_key: Optional[str] = None,
    model_name: Optional[str] = None,
    custom_prompt: Optional[str] = None,
) -> str:
    """
    Analyze a video file using Google Gemini API with focus on detecting concerning content.
    
    Performs comprehensive analysis to identify:
    - Misinformation and factual inaccuracies
    - Propaganda and manipulation techniques
    - Agenda-pushing and bias
    - Conspiracy theories and extremism
    
    Args:
        video_path: Path to the MP4 video file to analyze
        api_key: Google AI API key. If None, reads from GEMINI_API_KEY environment variable
        model_name: Gemini model to use. If None, uses GEMINI_MODEL_VIDEO from config
        custom_prompt: Optional custom prompt for analysis. If None, uses SEMANTIC_ANALYSIS_PROMPT
        
    Returns:
        Formatted string containing detailed video analysis with risk assessment
        
    Raises:
        FileNotFoundError: If video file doesn't exist
        ValueError: If API key is not provided or found in environment
        Exception: For API errors during upload or generation
    """
    # Use default model from config if not specified
    if model_name is None:
        model_name = GEMINI_MODEL_VIDEO
    # Validate video file exists
    video_file = Path(video_path)
    if not video_file.exists():
        raise FileNotFoundError(f"Video file not found: {video_path}")
    
    # Get API key
    if api_key is None:
        api_key = os.environ.get("GEMINI_API_KEY")
    if not api_key:
        raise ValueError(
            "GEMINI_API_KEY not found. Please provide api_key parameter or set GEMINI_API_KEY environment variable"
        )
    
    # Configure Gemini API
    genai.configure(api_key=api_key)
    
    # Upload video file
    start = time.time()
    print(f"Uploading video: {video_file.name}...")
    video_file_obj = genai.upload_file(path=str(video_file))
    end = time.time()
    print(f"Video uploaded in {end - start} seconds")

    print("Processing video...")
    while video_file_obj.state.name == "PROCESSING":
        time.sleep(2)
        video_file_obj = genai.get_file(video_file_obj.name)
    
    if video_file_obj.state.name == "FAILED":
        raise Exception(f"Video processing failed: {video_file_obj.state}")
    
    print("Video ready for analysis...")
    
    # Use default prompt from config if not specified
    prompt = custom_prompt if custom_prompt is not None else SEMANTIC_ANALYSIS_PROMPT
    
    # Create model and generate analysis
    model = genai.GenerativeModel(model_name=model_name)
    
    print("Generating analysis...")
    start = time.time()
    response = model.generate_content([video_file_obj, prompt])
    end = time.time()
    print(f"Analysis generated in {end - start} seconds")
    # Format the result
    analysis = response.text
    
    # Add metadata header
    formatted_result = get_formatted_result(analysis, video_file, video_path, model_name)
    
    # Clean up uploaded file
    try:
        genai.delete_file(video_file_obj.name)
        print("Cleaned up uploaded file from Gemini servers")
    except Exception as e:
        print(f"Warning: Could not delete uploaded file: {e}")
    
    return formatted_result.strip()


def analyze_video_quick(video_path: str, api_key: Optional[str] = None) -> str:
    """
    Quick video analysis focused on identifying red flags and risk level.
    
    Args:
        video_path: Path to the MP4 video file
        api_key: Google AI API key (optional, reads from env)
        
    Returns:
        Concise video analysis string with risk assessment
    """
    return analyze_video(
        video_path=video_path,
        api_key=api_key,
        custom_prompt=SEMANTIC_ANALYSIS_QUICK_PROMPT,
    )


if __name__ == "__main__":
    # Example usage
    import sys
    
    if len(sys.argv) < 2:
        print("Usage: python semantic_analysis.py <path_to_video.mp4>")
        sys.exit(1)
    
    video_path = sys.argv[1]
    
    try:
        analysis = analyze_video(video_path)
        print(analysis)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

```

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