# Project export: ReFocus

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

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: ReFocus helps you retrain your feed by skipping suggestive or distracting content—so you can rebuild a healthier digital environment. A clear mind starts with a clear feed.
- Devpost: https://devpost.com/software/refocus-a0hr1v
- GitHub: https://github.com/Steven-Hsu1/CalHacks/
- Team: 2 GitHub contributor(s) — Steven Hsu (1 commits), emilio lopez (1 commits)

## Devpost submission (written by the team)

### Inspiration

Social media algorithms shape what we see, often reinforcing the habits we’re trying to break. Just like your environment and the people around you influence your growth, your feed does too. ReFocus was inspired by the idea that recovery and self-improvement start with what surrounds you online. We wanted to build a way to take control back from the algorithm and help users create a cleaner, healthier digital space.

### What it does

ReFocus helps users retrain their social media feeds by identifying and skipping suggestive or distracting content. It works by detecting "trigger" categories—content types the user wants to avoid—and automatically acting on them. For example, if a user is trying to avoid suggestive videos, ReFocus helps skip and filter out those posts, gradually teaching the platform’s algorithm to show less of that content over time.

### How we built it

We built ReFocus using LiveKit for real-time screen sharing and interaction. When a user shares their screen, the app connects to a Node.js server that creates a new WebRTC room. From there, we used RoomIO for handling communication between the agent and user participants through audio and video tracks. We enabled live video input via RoomInputOptions(video_enabled=True), allowing the agent to receive frames from the user’s screen and classify them at regular intervals (1 FPS while speaking, 1 frame every 3 seconds otherwise). These frames are resized to 1024×1024 and encoded as JPEG for model processing. The backend uses FastAPI (Python) to facilitate interactions between the AI agent and the client, handling classification requests and trigger detection.

### Challenges we ran into

Getting LiveKit to run smoothly under unstable network conditions (pro tip: don’t test with bad Wi-Fi). Getting LiveKit to run smoothly under unstable network conditions (pro tip: don’t test with bad Wi-Fi). Integrating WebRTC and ensuring real-time responsiveness across browser environments. Integrating WebRTC and ensuring real-time responsiveness across browser environments. Managing the workflow logic — deciding when and where the agent should click, skip, or observe. Managing the workflow logic — deciding when and where the agent should click, skip, or observe.

### Accomplishments we're proud of

Successfully got LiveKit working end-to-end with real-time screen streaming. Successfully got LiveKit working end-to-end with real-time screen streaming. Built a functioning pipeline for detecting visual triggers and responding automatically. Built a functioning pipeline for detecting visual triggers and responding automatically. Established a foundation for behavior-driven feed retraining. Established a foundation for behavior-driven feed retraining. Demonstrated that feed detoxification can be automated in a way that supports recovery and focus. Demonstrated that feed detoxification can be automated in a way that supports recovery and focus.

### What we learned

We learned how powerful agent-driven automation can be when combined with real-time video and audio analysis. We also realized how critical workflow design is for making automation safe, ethical, and responsive. Most importantly, we gained insight into the potential of using technology not just for engagement—but for digital recovery and mindfulness.

### What's next

Expand our content classification model to detect a broader range of triggers. Expand our content classification model to detect a broader range of triggers. Refine agent interactions to improve accuracy and reliability across platforms. Refine agent interactions to improve accuracy and reliability across platforms. Add user customization features for defining personal triggers and recovery goals. Add user customization features for defining personal triggers and recovery goals. Eventually, create a browser extension that passively retrains your feed as you scroll—helping you build a healthier digital environment, one skip at a time. Eventually, create a browser extension that passively retrains your feed as you scroll—helping you build a healthier digital environment, one skip at a time.

## README (from the GitHub repository)

# Content Filter - Take Control of Your Feed

**Cal Hacks 2024 Project**

A Chrome extension powered by AI that gives users control over social media content algorithms. Filter out unwanted content using natural language descriptions.

## Overview

Social media algorithms are like black boxes - users have little control over what they see. This project empowers users to take back control by specifying content they don't want to see in natural language. The system uses AI vision models to analyze video content in real-time and automatically clicks "Not interested" when triggers are detected.

## Features

- **Natural Language Filters**: Describe what you don't want to see (e.g., "smoking", "violence")
- **Real-time Video Analysis**: AI analyzes video frames as you browse
- **Multi-platform Support**: Works on YouTube, Instagram, TikTok, Facebook, Twitter, and more
- **Privacy-focused**: Video analysis happens in real-time, no data stored
- **Intelligent Button Detection**: Automatically finds and clicks "Not interested" buttons
- **Visual Feedback**: Track how many items have been filtered

## Technology Stack

### Chrome Extension
- JavaScript/TypeScript
- Chrome Extension API (Manifest V3)
- WebRTC for screen capture
- LiveKit Client SDK

### LiveKit Agent (Python)
- LiveKit Agents SDK
- Anthropic Claude 3.5 Sonnet (vision model)
- PIL/Pillow for image processing
- Bright Data MCP for DOM intelligence

### Communication
- LiveKit Cloud for real-time WebRTC
- Data channels for bidirectional messaging

## Architecture

```
┌─────────────────────────────────────┐
│     Chrome Extension                │
│  ┌──────────────────────────────┐  │
│  │  Popup UI (Trigger Input)    │  │
│  └──────────────────────────────┘  │
│  ┌──────────────────────────────┐  │
│  │  Content Script              │  │
│  │  (DOM Interaction)           │  │
│  └──────────────────────────────┘  │
│  ┌──────────────────────────────┐  │
│  │  Background Service Worker   │  │
│  │  (Screen Capture + WebRTC)   │  │
│  └──────────────────────────────┘  │
└────────────┬────────────────────────┘
             │ WebRTC Video Stream
             ▼
┌─────────────────────────────────────┐
│   LiveKit Cloud                     │
│  ┌──────────────────────────────┐  │
│  │  LiveKit Agent (Python)      │  │
│  │  - Video Analysis            │  │
│  │  - Trigger Detection         │  │
│  │  - Command Generation        │  │
│  └────────┬─────────────────────┘  │
└───────────┼─────────────────────────┘
            │
            ▼
┌─────────────────────────────────────┐
│   Bright Data MCP Server            │
│   - Webpage Context                 │
│   - DOM Element Location            │
└─────────────────────────────────────┘
```

## Quick Start

### Prerequisites

1. **LiveKit Cloud Account** - https://livekit.io
2. **Anthropic Claude API Key** - https://console.anthropic.com
3. **Node.js 18+** and **Python 3.10+**
4. **UV** (Python package manager) - https://astral.sh/uv

### Setup

1. **Clone the repository**
   ```bash
   cd Calhacks
   ```

2. **Configure environment**
   ```bash
   cp .env.example .env
   # Edit .env with your API keys
   ```

3. **Set up the agent**
   ```bash
   cd agent
   uv pip install -r requirements.txt
   python main.py
   ```

4. **Build the extension**
   ```bash
   cd extension
   npm install
   npm run build
   ```

5. **Load extension in Chrome**
   - Go to `chrome://extensions/`
   - Enable "Developer mode"
   - Click "Load unpacked"
   - Select `extension/dist` folder

6. **Start filtering!**
   - Click the extension icon
   - Add your content filters
   - Click "Start Monitoring"
   - Browse social media

## Documentation

- **[Setup Guide](docs/SETUP.md)** - Detailed setup instructions
- **[API Documentation](docs/API.md)** - API reference and message protocols
- **[Implementation Plan](plan.md)** - Complete implementation details

## Project Structure

```
Calhacks/
├── extension/          # Chrome Extension
│   ├── popup/         # UI for managing filters
│   ├── content/       # Content scripts for DOM
│   ├── background/    # Service worker + WebRTC
│   └── lib/           # Shared utilities
│
├── agent/             # LiveKit Agent (Python)
│   ├── main.py       # Agent entry point
│   ├── video_analyzer.py    # Vision LLM integration
│   ├── mcp_client.py        # Bright Data MCP
│   └── command_sender.py    # Extension communication
│
├── docs/              # Documentation
├── mcp-config/        # MCP server config
└── plan.md           # Detailed implementation plan
```

## How It Works

1. **User inputs filters**: User describes unwanted content in natural language (e.g., "smoking", "violence")

2. **Extension captures screen**: When monitoring starts, the extension captures the browser tab's video stream

3. **Stream to LiveKit**: Video is sent via WebRTC to LiveKit Cloud where the agent receives it

4. **AI analyzes frames**: The agent processes video frames using GPT-4V or Claude vision models

5. **Trigger detection**: When unwanted content is detected, the agent identifies it

6. **Find action button**: Agent uses Bright Data MCP to find "Not interested" buttons on the page

7. **Execute action**: Agent sends click command back to extension, which executes the click

8. **Continue monitoring**: Process continues in real-time as user browses

## Supported Platforms

- ✅ YouTube (videos and shorts)
- ✅ Instagram (feed and reels)
- ✅ TikTok (For You page)
- ✅ Facebook (feed)
- ✅ Twitter/X (timeline)
- ✅ Reddit (feed)
- ✅ Generic support for other platforms

## Configuration

See `.env.example` for all configuration options:

```env
# Required
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_key
LIVEKIT_API_SECRET=your_secret
ANTHROPIC_API_KEY=sk-ant-...

# Optional
FRAME_SKIP_COUNT=2              # Process every 3rd frame
IMAGE_MAX_SIZE=1024             # Max image dimension
MIN_CONFIDENCE_THRESHOLD=0.7    # Detection threshold
```

## Cost Estimates

- **Anthropic Claude 3.5 Sonnet**: ~$0.015 per 1000 frames
- **LiveKit Cloud**: Free tier available, then $0.01/min

Tips to reduce costs:
- Increase `FRAME_SKIP_COUNT`
- Reduce `IMAGE_MAX_SIZE`
- Use Claude 3 Haiku for a cheaper, faster option

## Development

### Building Extension
```bash
cd extension
npm run dev    # Watch mode for development
npm run build  # Production build
```

### Running Agent
```bash
cd agent
python main.py

# Or with debug logging
LOG_LEVEL=DEBUG python main.py
```

### Debugging
- Extension logs: Chrome DevTools Console (F12)
- Agent logs: Terminal output
- LiveKit dashboard: https://cloud.livekit.io

## Known Limitations

- Requires screen capture permission
- Vision API costs for high usage
- Detection accuracy depends on model quality
- May not work on all websites due to CSP policies
- Requires active internet connection

## Future Enhancements

- [ ] Local ML inference (reduce API costs)
- [ ] Mobile browser support
- [ ] Collaborative filter lists
- [ ] Advanced rules (time-based, contextual)
- [ ] Performance optimizations
- [ ] Multi-language support

## Contributing

This is a Cal Hacks 2024 hackathon project. Contributions and suggestions are welcome!

## Privacy & Security

- Video frames are processed in real-time
- No video data is stored or persisted
- User filters are stored locally in Chrome storage
- All communications use encrypted WebRTC/WSS
- API keys should be kept secure and rotated regularly

## License

This project was created for Cal Hacks 2024.

## Team

Built with ❤️ for Cal Hacks 2024

## Acknowledgments

- **LiveKit** - Real-time communication infrastructure
- **OpenAI/Anthropic** - Vision AI models
- **Bright Data** - Web intelligence via MCP
- **Cal Hacks** - For hosting an amazing hackathon!

## Support

For issues or questions:
- Check [docs/SETUP.md](docs/SETUP.md) for troubleshooting
- Review [docs/API.md](docs/API.md) for technical details
- Check [plan.md](plan.md) for implementation details

---

**Note**: This pro

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 22 recognized source files, 142 KB.
- CSS (language) — 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
- FastAPI (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (28 of 28)

```
.gitignore
agent/ai_navigator.py
agent/command_sender.py
agent/config.py
agent/main.py
agent/pyproject.toml
agent/README.md
agent/requirements.txt
agent/scroll_handler.py
agent/test_connection.py
agent/uv.lock
agent/video_analyzer.py
extension/background/background.js
extension/content/content.js
extension/manifest.json
extension/offscreen/offscreen.html
extension/offscreen/offscreen.js
extension/package.json
extension/popup/popup.css
extension/popup/popup.html
extension/popup/popup.js
extension/test-connection.js
extension/test-token.js
extension/token-server.js
extension/webpack.config.js
README.md
TIKTOK_WORKFLOW.md
TRIGGER_FIX.md
```

### Dependencies

- agent/pyproject.toml: aiohttp@>=3.9.0, black@>=23.0.0, livekit@>=0.11.0, livekit-agents[anthropic]@>=1.2, openai@>=1.0.0, pillow@>=10.0.0, pytest@>=7.0.0, python-dotenv@>=1.0.0, ruff@>=0.1.0
- agent/requirements.txt: aiohttp@>=3.9.0, livekit@>=0.11.0, livekit-agents@>=1.2, openai@>=1.30.0, pillow@>=10.0.0, python-dotenv@>=1.0.0, uvloop@>=0.19.0
- extension/package.json: copy-webpack-plugin@^11.0.0, dotenv@^16.4.7, livekit-client@^2.0.0, livekit-server-sdk@^2.6.1, webpack@^5.89.0, webpack-cli@^5.1.4

### Recent commits (newest first)

- extension
- changes
- changes
- v1
- ignore vim, env
- init

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

### TRIGGER_FIX.md

```markdown
# Fixed: False Positive Trigger Detection

## Problem
The system was clicking "not interested" on ALL videos because OpenAI was detecting triggers too broadly. For example, with trigger "valorant", it would detect ANY gaming content as "related to valorant".

## Solution Implemented

### 1. Stricter Trigger Detection Prompt (video_analyzer.py)

**Before:** Detected "Any symbols or imagery related to the triggers"

**After:** Only detects if trigger is EXPLICITLY present:
- The exact word must be visible in text/captions
- The logo/brand must be clearly shown
- The actual thing itself must be present

### 2. Higher Confidence Threshold
- Changed from 0.7 to 0.85
- Added "Be VERY conservative - when in doubt, set trigger_detected to false"

### 3. Clear Examples in Prompt
```
- Trigger "valorant": ONLY detect if you see the word "Valorant", Valorant logo, or actual Valorant gameplay
- NOT other FPS games or gaming content in general
```

### 4. Reset Timer After Trigger
- When "not interested" is clicked, reset video timer
- Prevents trying to navigate again after handling trigger

## Correct Workflow Now

### Normal Video (No Trigger):
1. Video plays
2. After 15 seconds → Press Down Arrow to skip
3. Next video starts

### Video With Trigger (e.g., "valorant"):
1. Video plays
2. If "Valorant" explicitly shown/mentioned → Click "Not interested"
3. Next video starts automatically

## Testing

1. **Set trigger:** Add "valorant" in extension
2. **Watch TikTok:** Most videos should play for 15s then skip
3. **Trigger test:** Only videos that EXPLICITLY show Valorant should trigger

## Performance

- **Frame rate:** 2 FPS (checks twice per second)
- **Response time:** 0.5-1 second to detect trigger
- **Skip time:** 15 seconds for non-trigger videos
- **Click delay:** 50ms for fast clicking

## Common False Positives (Now Fixed)

With trigger "valorant", these should NO LONGER trigger:
- ❌ Other FPS games (CS:GO, Overwatch, etc.)
- ❌ General gaming content
- ❌ Esports content without Valorant
- ❌ Similar looking games

Only triggers when:
- ✅ "Valorant" text visible
- ✅ Valorant logo shown
- ✅ Actual Valorant gameplay
- ✅ Someone saying "Valorant"
```

### TIKTOK_WORKFLOW.md

```markdown
# TikTok Content Filter Workflow

## Current Implementation (Fixed)

### TikTok Button Selectors

**Three Dots Button (More Actions):**
```css
button.TUXButton.TUXButton--capsule.TUXButton--medium.TUXButton--secondary.action-item.css-7a914j
```

**Not Interested Menu Item:**
```css
div.TUXMenuItem[data-e2e="more-menu-popover_not-interested"]
```

**Next Video Button:**
```css
button.TUXButton.TUXButton--capsule.TUXButton--medium.TUXButton--secondary.action-item.css-16m89jc
```

## Workflow Logic

### 1. When Trigger IS Detected

If a trigger word (e.g., "valorant") is detected in the video:

1. **Click Three Dots** → Opens menu
2. **Wait 800ms** → Let menu appear
3. **Click "Not Interested"** → Removes video from feed
4. **Reset timer** → Start tracking new video

### 2. When NO Trigger + Video Finished (30s)

If no trigger is detected AND video has been watched for 30 seconds:

1. **Click Next Video Button** → Skip to next video
2. **If button not found** → Fallback to scroll down
3. **Reset timer** → Start tracking new video

## Configuration

### Video Watch Duration

Default: **30 seconds** (configurable)

Set in `.env`:
```
MAX_VIDEO_WATCH_DURATION=30
```

### Frame Analysis Rate

Default: **1 frame per second**

## Debugging

### Check Agent Logs

When trigger detected:
```
🚨 TRIGGER DETECTED: valorant (confidence: 0.85)
🔧 Sending TikTok two-step 'Not interested' click commands to extension...
Step 1: Sending click command for 3 dots button: button.TUXButton...css-7a914j
Step 2: Sending click command for 'Not interested': div.TUXMenuItem[data-e2e="more-menu-popover_not-interested"]
```

When no trigger + video ends:
```
⏱️  TikTok video watched for 30.2s (max: 30s)
✅ No trigger detected in this video - skipping to next
📤 Sending click command for TikTok next video button: button.TUXButton...css-16m89jc
```

### Check Extension Console

Success:
```
[Content] Attempting to click selector: button.TUXButton...css-7a914j
[Content] Found element: button.TUXButton...
[Content] ✅ Clicked: button.TUXButton...css-7a914j
```

Failure:
```
[Content] ⚠️  Could not find element with selector: button.TUXButton...
[Content] No next button found, scrolling down instead
```

## Common Issues

### Buttons Not Found

**Possible causes:**
1. TikTok updated their UI classes
2. Page not fully loaded
3. Video player in different state

**Fix:**
- Inspect element in Chrome DevTools
- Update selectors in `agent/main.py` and `extension/background/background.js`

### Video End Not Detected

**Possible causes:**
1. Timer not resetting after navigation
2. Video shorter than 30 seconds

**Fix:**
- Check `self.video_start_time` is reset after each navigation
- Adjust `MAX_VIDEO_WATCH_DURATION` in `.env`

### Triggers Not Working

**Possible causes:**
1. Triggers not set in extension popup
2. OpenAI not detecting the trigger word

**Fix:**
- Check extension popup has triggers listed
- Check agent logs for "Initialized with N trigger(s)"
- Verify OpenAI is returning exa
[truncated — 379 more characters]
```

### agent/requirements.txt

```
# LiveKit dependencies
livekit>=0.11.0
livekit-agents>=1.2

# LLM - OpenAI for vision
openai>=1.30.0

# Image processing
pillow>=10.0.0

# Utilities
python-dotenv>=1.0.0
aiohttp>=3.9.0

# Optional: For better async performance
uvloop>=0.19.0; sys_platform != 'win32'

```

### extension/package.json

```
{
  "name": "content-filter-extension",
  "version": "1.0.0",
  "description": "Chrome extension for content filtering with AI",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development --watch",
    "clean": "rm -rf dist"
  },
  "devDependencies": {
    "copy-webpack-plugin": "^11.0.0",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4"
  },
  "dependencies": {
    "dotenv": "^16.4.7",
    "livekit-client": "^2.0.0",
    "livekit-server-sdk": "^2.6.1"
  }
}

```

### agent/pyproject.toml

```
[project]
name = "content-filter-agent"
version = "1.0.0"
description = "LiveKit agent for content filtering with AI vision"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "livekit>=0.11.0",
    "livekit-agents[anthropic]>=1.2",
    "openai>=1.0.0",
    "pillow>=10.0.0",
    "python-dotenv>=1.0.0",
    "aiohttp>=3.9.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black>=23.0.0",
    "ruff>=0.1.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["."]

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.black]
line-length = 100
target-version = ["py310"]

```

### agent/main.py

```python
"""
Content Filter Agent - Main Entry Point
Processes video streams from Chrome extension and detects content triggers
"""

import asyncio
import logging
import os
import json
from typing import Optional
from dotenv import load_dotenv
from livekit import agents, rtc
from livekit.agents import JobContext, WorkerOptions, cli

from video_analyzer import VideoAnalyzer
from command_sender import CommandSender
from scroll_handler import ScrollHandler
from ai_navigator import AINavigator
import time

load_dotenv()

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# Suppress verbose logging from libraries to avoid image data in logs
logging.getLogger("openai").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)

logger = logging.getLogger(__name__)


class ContentFilterAgent:
    """Main agent class that coordinates video analysis and content filtering"""

    def __init__(self):
        self.triggers = []
        # Don't initialize these here - they will be initialized lazily in entrypoint
        # to avoid pickling issues in dev mode
        self.video_analyzer = None
        self.command_sender = None
        self.scroll_handler = None
        self.ai_navigator = None
        self.processing_frame = False
        self.current_url = None  # Track the current page URL
        self.last_scroll_time = time.time()

        # Time-based video tracking for auto-looping platforms (like TikTok)
        self.video_start_time = time.time()
        # TikTok videos are typically 15-60s, default to 10s for faster navigation
        self.max_video_watch_duration = float(os.getenv("MAX_VIDEO_WATCH_DURATION", "10"))

    async def entrypoint(self, ctx: JobContext):
        """Agent entry point when participant joins room"""
        # Initialize components here (after multiprocessing fork in dev mode)
        if self.video_analyzer is None:
            self.video_analyzer = VideoAnalyzer()
            self.command_sender = CommandSender()
            self.scroll_handler = ScrollHandler()
            self.ai_navigator = AINavigator()

        logger.info(f"Starting Content Filter Agent for room {ctx.room.name}")

        # Log that we're using extension for all actions
        logger.info("✅ Using Chrome extension for all click actions (TikTok-optimized)")

        # Log video watch duration configuration
        logger.info(f"⏱️  Max video watch duration: {self.max_video_watch_duration}s (auto-navigate after this time)")

        # Connect to the room
        await ctx.connect()
        logger.info("✅ Connected to room successfully")

        # Set up event handlers BEFORE waiting for participants

        # Listen for participants connecting
        @ctx.room.on("participant_connected")
        def on_participant_connected(participant: rtc.RemoteParticipant):
            logger.info(f"👤 Participant connected: {participant.identity}")
            logger.info(f"📊 Participant has {len(participant.track_publications)} track publications")

        # Listen for track publications
        @ctx.room.on("track_published")
        def on_track_published(
            publication: rtc.RemoteTrackPublication,
            participant: rtc.RemoteParticipant,
        ):
            logger.info(f"📹 Track published by {participant.identity}")
            logger.info(f"   Track: {publication.sid}, Kind: {publication.kind}, Source: {publication.source}")

        # Listen for video tracks being subscribed
        @ctx.room.on("track_subscribed")
        def on_track_subscribed(
            track: rtc.Track,
            publication: rtc.TrackPublication,
            participant: rtc.RemoteParticipant,
        ):
            logger.info(f"🎥 Track subscribed from {participant.identity}")
            logger.info(f"   Track kind: {track.kind}, SID: {track.sid}")
            if track.kind == rtc.TrackKind.KIND_VIDEO:
                logger.info("✅ Video track subscribed, starting analysis")
                asyncio.create_task(
                    self.process_video_track(ctx, track, participant)
                )

        # Listen for data messages (triggers from extension)
        @ctx.room.on("data_received")
        def on_data_received(data_packet: rtc.DataPacket):
            logger.info(f"📨 Data received from {data_packet.participant.identity if data_packet.participant else 'unknown'}")
            asyncio.create_task(self.handle_data_message(ctx, data_packet.data, data_packet.participant))

        logger.info("🔧 Event handlers configured")
        logger.info("⏳ Waiting for participants to join...")

        # Wait for participant to join
        participant = await ctx.wait_for_participant()
        logger.info(f"✅ Participant joined: {participant.identity}")
        logger.info(f"📊 Participant info:")
        logger.info(f"   - Identity: {participant.identity}")
        logger.info(f"   - SID: {participant.sid}")
        logger.info(f"   - Track publications: {len(participant.track_publications)}")

        # Check for existing track publications (handles race condition)
        if participant.track_publications:
            logger.info("🔍 Checking for existing video tracks...")
            for sid, publication in participant.track_publications.items():
                logger.info(f"   Found track: {sid}, Kind: {publication.kind}, Subscribed: {publication.subscribed}")

                # If it's a video track and we have the track object, start processing
                if publication.kind == rtc.TrackKind.KIND_VIDEO:
                    if publication.track:
                        logger.info(f"✅ Starting analysis of existing video track: {sid}")
                        asyncio.create_task(
                            self.process_video_track(ctx, publication.track, participant)
                        )
        
[truncated — 15263 more characters]
```

### extension/webpack.config.js

```javascript
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: {
    background: './background/background.js',
    content: './content/content.js',
    popup: './popup/popup.js',
    offscreen: './offscreen/offscreen.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: (pathData) => {
      // Special handling for offscreen to put it in offscreen/offscreen.js
      if (pathData.chunk.name === 'offscreen') {
        return 'offscreen/offscreen.js';
      }
      return '[name]/[name].js';
    },
    clean: true
  },
  plugins: [
    new CopyPlugin({
      patterns: [
        { from: 'manifest.json', to: 'manifest.json' },
        { from: 'popup/popup.html', to: 'popup/popup.html' },
        { from: 'popup/popup.css', to: 'popup/popup.css' },
        { from: 'offscreen/offscreen.html', to: 'offscreen/offscreen.html' },
        { from: 'icons', to: 'icons', noErrorOnMissing: true }
      ]
    })
  ],
  resolve: {
    extensions: ['.js'],
    modules: [path.resolve(__dirname, 'node_modules')]
  }
};

```

### agent/test_connection.py

```python
"""Test LiveKit connection"""
import os
import asyncio
from dotenv import load_dotenv
from livekit import api

load_dotenv()

async def test_connection():
    """Test if we can connect to LiveKit"""
    print("Testing LiveKit connection...")

    url = os.getenv("LIVEKIT_URL")
    api_key = os.getenv("LIVEKIT_API_KEY")
    api_secret = os.getenv("LIVEKIT_API_SECRET")

    print(f"URL: {url}")
    print(f"API Key: {api_key[:10]}...")

    try:
        # Create LiveKit API client
        lk_api = api.LiveKitAPI(
            url,
            api_key,
            api_secret,
        )

        # Try to list rooms
        print("\nAttempting to list rooms...")
        from livekit.api import ListRoomsRequest
        rooms = await lk_api.room.list_rooms(ListRoomsRequest())
        print(f"✓ Successfully connected! Found {len(rooms)} rooms")

        for room in rooms:
            print(f"  - Room: {room.name} (participants: {room.num_participants})")

        # Try to list participants in a test room
        print("\nConnection test successful!")
        await lk_api.aclose()
        return True

    except Exception as e:
        print(f"✗ Connection failed: {type(e).__name__}: {e}")
        return False

if __name__ == "__main__":
    asyncio.run(test_connection())

```

### agent/config.py

```python
"""
Configuration for Content Filter Agent
"""

import os
from typing import Optional


class Config:
    """Agent configuration"""

    # LiveKit Configuration
    LIVEKIT_URL: str = os.getenv("LIVEKIT_URL", "")
    LIVEKIT_API_KEY: str = os.getenv("LIVEKIT_API_KEY", "")
    LIVEKIT_API_SECRET: str = os.getenv("LIVEKIT_API_SECRET", "")

    # Vision LLM Configuration
    OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")

    # Vision Model Settings
    VISION_MODEL: str = "gpt-4o"
    VISION_MAX_TOKENS: int = 300
    VISION_TEMPERATURE: float = 0.1

    # Agent Settings
    FPS_LIMIT: int = int(os.getenv("FPS_LIMIT", "1"))  # Process N frames per second (default: 1 FPS)
    IMAGE_MAX_SIZE: int = int(os.getenv("IMAGE_MAX_SIZE", "1024"))  # Max dimension for analysis
    IMAGE_QUALITY: int = int(os.getenv("IMAGE_QUALITY", "85"))  # JPEG quality (1-100)

    # Detection Settings
    MIN_CONFIDENCE_THRESHOLD: float = float(os.getenv("MIN_CONFIDENCE_THRESHOLD", "0.7"))

    # Logging
    LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")

    @classmethod
    def validate(cls) -> bool:
        """
        Validate that all required configuration is present

        Returns:
            True if configuration is valid
        """
        errors = []

        if not cls.LIVEKIT_URL:
            errors.append("LIVEKIT_URL is not set")
        if not cls.LIVEKIT_API_KEY:
            errors.append("LIVEKIT_API_KEY is not set")
        if not cls.LIVEKIT_API_SECRET:
            errors.append("LIVEKIT_API_SECRET is not set")

        if not cls.OPENAI_API_KEY:
            errors.append("OPENAI_API_KEY is not set")

        if errors:
            print("Configuration errors:")
            for error in errors:
                print(f"  - {error}")
            return False

        return True

    @classmethod
    def print_config(cls):
        """Print current configuration (without secrets)"""
        print("Agent Configuration:")
        print(f"  LiveKit URL: {cls.LIVEKIT_URL}")
        print(f"  LiveKit API Key: {'*' * 20 if cls.LIVEKIT_API_KEY else 'NOT SET'}")
        print(f"  Vision Provider: OpenAI GPT-4o")
        print(f"  Vision Model: {cls.VISION_MODEL}")
        print(f"  OpenAI API Key: {'*' * 20 if cls.OPENAI_API_KEY else 'NOT SET'}")
        print(f"  FPS Limit: {cls.FPS_LIMIT} frame(s) per second")
        print(f"  Image Size: {cls.IMAGE_MAX_SIZE}px")
        print(f"  Min Confidence: {cls.MIN_CONFIDENCE_THRESHOLD}")


# Export singleton instance
config = Config()

```

### extension/token-server.js

```javascript
/**
 * Simple Token Server for LiveKit
 * Run this with: node token-server.js
 */

const http = require('http');
const { AccessToken } = require('livekit-server-sdk');
require('dotenv').config({ path: '../.env' });

const PORT = 3000;

// Load from environment
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY;
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET;

if (!LIVEKIT_API_KEY || !LIVEKIT_API_SECRET) {
  console.error('❌ Missing LIVEKIT_API_KEY or LIVEKIT_API_SECRET in .env file');
  process.exit(1);
}

const server = http.createServer((req, res) => {
  // Enable CORS
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  if (req.url === '/token' && req.method === 'POST') {
    let body = '';

    req.on('data', chunk => {
      body += chunk.toString();
    });

    req.on('end', async () => {
      try {
        const data = body ? JSON.parse(body) : {};
        const roomName = data.room || 'content-filter';
        const participantName = data.identity || `extension-${Date.now()}`;

        // Create access token
        const token = new AccessToken(
          LIVEKIT_API_KEY,
          LIVEKIT_API_SECRET,
          {
            identity: participantName,
            ttl: '6h'
          }
        );

        // Grant permissions
        token.addGrant({
          roomJoin: true,
          room: roomName,
          canPublish: true,
          canSubscribe: true,
          canPublishData: true
        });

        // toJwt() returns a Promise in v2.6+
        const jwt = await token.toJwt();

        console.log(`✅ Generated token for ${participantName} in room ${roomName}`);

        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          token: jwt,
          room: roomName,
          identity: participantName
        }));
      } catch (error) {
        console.error('❌ Error generating token:', error);
        res.writeHead(500, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: error.message }));
      }
    });
  } else {
    res.writeHead(404);
    res.end('Not found');
  }
});

server.listen(PORT, () => {
  console.log(`🚀 LiveKit Token Server running on http://localhost:${PORT}`);
  console.log(`📍 Endpoint: POST http://localhost:${PORT}/token`);
  console.log(`🔑 Using API Key: ${LIVEKIT_API_KEY.substring(0, 10)}...`);
});

```

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