# Project export: Artifact

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: Artifact verifies the authenticity of digital videos using AI, detecting deepfakes and manipulated content in real time to restore trust in what we see online.
- Devpost: https://devpost.com/software/artifact-4sw8x7
- GitHub: https://github.com/ArtiFACT-CalHacks/artifact
- Video: https://www.youtube.com/embed/ov8qmpLXYac?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Arohan Deshpande (2 commits), RohanShah06 (2 commits), Rohan Shah (2 commits)

## Devpost submission (written by the team)

### Inspiration

With the rise of tools like Sora AI, the internet is being flooded with AI-generated videos — many of which blur the line between creativity and misinformation. We noticed how difficult it’s become for users to tell what’s real and what’s artificially created. This inspired us to build Artifact, a tool that helps bring transparency and accountability back to digital media.

### What it does

Artifact analyzes videos to determine whether they are AI-generated or authentic. Users can upload a clip or provide an input, and our system classifies it with high accuracy using a frame-by-frame approach. Alongside detection, Artifact also curates a feed of verified AI-generated content, allowing users to explore how synthetic media is evolving in real time.

### How we built it

We combined two computer vision models for image classification, using a frame segmentation pipeline to analyze videos at the micro level. Our backend processes each frame, computes classification scores, and aggregates the results to deliver a final verdict. For the frontend, we used Snapdev to create a clean, responsive interface that makes interacting with the model fast and intuitive.

### Challenges we ran into

One of the biggest challenges was the lack of robust datasets for AI-generated video detection. Most open datasets are focused on static deepfakes or images, so we had to adapt and generate our own samples. Additionally, optimizing model performance across multiple architectures proved difficult given limited time and resources.

### Accomplishments we're proud of

We’re proud of achieving high accuracy and reliability despite limited data and time. Building a working prototype that not only detects AI-generated videos but also curates verified content is something we’re really excited about. It represents a meaningful step toward restoring trust in digital media.

### What we learned

We learned to persevere through uncertainty — when existing solutions or datasets didn’t meet our needs, we created our own. This project taught us valuable lessons in problem-solving, dataset engineering, and model fine-tuning, as well as the importance of teamwork when innovating in new and undefined problem spaces.

### What's next

Our next step is to enable video link verification, allowing users to paste URLs instead of uploading files. However, implementing this safely without violating copyright restrictions is a challenge we’ll need to navigate carefully. Long-term, we hope to expand Artifact into a browser plugin or public verification API, empowering everyone to validate digital media effortlessly.

## README (from the GitHub repository)

# Binary AI Video Classification

A simple binary classifier to detect AI-generated videos using EfficientNet-V2-L and ConvNeXt-Base models with adaptive frame extraction.

## Setup

1. Install dependencies:
```bash
pip install -r requirements.txt
```

2. Set up Hugging Face authentication (optional):
```bash
export HUGGINGFACE_TOKEN=your_token_here
```

## Usage

### Step 1: Extract Frames
Download the dataset and extract frames adaptively:
```bash
python frame_extractor.py --download --output ./data_frames
```

This will:
- Download the DeepAction dataset locally
- Extract 1-7 frames per video based on duration
- Save frames as JPG images in `./data_frames/`

### Step 2: Training
Train both models on the extracted frames:
```bash
python train.py
```

### Step 3: Evaluation
Test trained models and see confidence scores:
```bash
python evaluate.py
```

## Models

- **EfficientNet-V2-L**: 145M parameters, pretrained on ImageNet
- **ConvNeXt-Base**: 88M parameters, pretrained on ImageNet

Both models are modified for 2-class binary classification (Real vs AI-generated).

## Dataset

Uses the `faridlab/deepaction_v1` dataset from Hugging Face, which contains:
- Real videos
- AI-generated videos

The system downloads videos locally and extracts frames adaptively:
- ≤1s: 1 frame
- ≤5s: 2 frames  
- ≤10s: 3 frames
- ≤20s: 5 frames
- ≤30s: 6 frames
- >30s: 7 frames (cap)

## Output

- Extracted frames saved to `data_frames/` directory
- Trained models saved to `models/` directory (only classifier weights)
- Training logs saved to `training.log`
- Evaluation shows accuracy, precision, recall, F1-score, and confidence scores

## Detected evidence (automated analysis)

Indexed codebase: 18 recognized source files, 78 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- Hugging Face (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Tailwind CSS (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (25 of 25)

```
.gitignore
backend/app.py
backend/inference.py
backend/model_loader.py
backend/README.md
backend/requirements.txt
backend/upload_handler.py
create_mock_dataset.py
env.example
evaluate.py
frame_extractor.py
index.html
INTEGRATION_GUIDE.md
package.json
README.md
requirements.txt
src/index.css
src/main.tsx
src/pages/Detect.tsx
src/utils/api.ts
test_api.py
train.py
tsconfig.json
tsconfig.node.json
vite.config.ts
```

### Dependencies

- backend/requirements.txt: Flask@==2.3.3, Flask-CORS@==4.0.0, imageio[ffmpeg]@>=2.31.0, numpy@>=1.24.0, Pillow@>=9.5.0, torch@>=2.2.0, torchvision@>=0.17.0, Werkzeug@>=2.3.7
- package.json: @types/react@^19.2.2, @types/react-dom@^19.2.2, @vitejs/plugin-react@^5.1.0, react@^19.2.0, react-dom@^19.2.0, typescript@^5.9.3, vite@^7.1.12
- requirements.txt: datasets@>=2.14.0, huggingface-hub@>=0.16.0, imageio[ffmpeg]@>=2.31.0, kagglehub@>=0.2.0, numpy@>=1.24.0, opencv-python@>=4.8.0, Pillow@>=9.5.0, python-dotenv@>=1.0.0, scikit-learn@>=1.3.0, torch@>=2.2.0, torchvision@>=0.17.0, tqdm@>=4.65.0, transformers@>=4.30.0

### Recent commits (newest first)

- Add frontend files for React + Vite setup
- Remove large data_frames files from tracking and update .gitignore
- Clean up backend branch: Add complete ML pipeline with local data processing
- Add complete Flask backend API and frontend integration
- Merge pull request #1 from ArtiFACT-CalHacks/rohan1
- mock data
- data
- chore: file organization
- Add complete training setup for both models
- Added dummy files to each folder
- Initial commit

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

### INTEGRATION_GUIDE.md

```markdown
# Frontend-Backend Integration Guide

## 🎯 Overview

This guide shows how to connect your React frontend with the Flask backend API for AI detection.

## 📁 Files Created

### Backend Files (in `/backend/`)
- `app.py` - Main Flask API server
- `model_loader.py` - EfficientNet model loading
- `inference.py` - Image/video inference engine
- `upload_handler.py` - File upload management
- `requirements.txt` - Python dependencies
- `README.md` - Backend documentation

### Frontend Integration Files
- `src/utils/api.ts` - Real API integration (replaces mock)
- `src/pages/Detect.tsx` - Updated Detect component
- `env.example` - Environment variables template

## 🚀 Quick Start

### 1. Start the Backend
```bash
cd backend
pip install -r requirements.txt
python3 app.py
```
Backend will run on `http://localhost:8000`

### 2. Configure Frontend Environment
```bash
# Copy environment template
cp env.example .env

# Edit .env file
VITE_API_BASE_URL=http://localhost:8000
```

### 3. Update Frontend Imports
In your existing `src/pages/Detect.tsx`, change:
```typescript
// From:
import { uploadFile, detectAuthenticity } from '@/utils/mockApi';

// To:
import { uploadFile, detectAuthenticity } from '@/utils/api';
```

## 🔌 API Endpoints

### Upload Endpoint
```
POST /api/upload
Content-Type: multipart/form-data

Request: file (image/video)
Response: { media_id: string, success: boolean }
```

### Detection Endpoint
```
POST /api/detect
Content-Type: application/json

Request: { media_id: string }
Response: { is_ai: boolean, confidence: number, success: boolean }
```

### Health Check
```
GET /api/health
Response: { status: "healthy", model_loaded: boolean }
```

## 🧪 Testing

### 1. Test Backend Health
```bash
curl http://localhost:8000/api/health
```

### 2. Test File Upload
```bash
curl -X POST -F "file=@test_image.jpg" http://localhost:8000/api/upload
```

### 3. Test Detection
```bash
curl -X POST -H "Content-Type: application/json" \
  -d '{"media_id":"your-media-id"}' \
  http://localhost:8000/api/detect
```

## 🔄 Switching Between Mock and Real API

### Use Real API (Production)
```typescript
import { uploadFile, detectAuthenticity } from '@/utils/api';
```

### Use Mock API (Development/Testing)
```typescript
import { uploadFile, detectAuthenticity } from '@/utils/mockApi';
```

## 🐛 Troubleshooting

### Backend Issues
- **Model not loading**: Check if `../models/effnetv2_test.pth` exists
- **Port conflicts**: Change port in `app.py` (line 80)
- **CORS errors**: Ensure Flask-CORS is installed

### Frontend Issues
- **API not connecting**: Check `VITE_API_BASE_URL` in `.env`
- **Upload fails**: Check file size limits (100MB max)
- **Detection fails**: Verify media_id is valid

### Common Errors
- **"File not found"**: Media ID doesn't exist or file was deleted
- **"Model not loaded"**: Backend model loading failed
- **"CORS error"**: Frontend/backend URL mismatch

## 📊 Response Format

### Upload Response
```typescript
interface UploadResponse {
  me
[truncated — 1771 more characters]
```

### requirements.txt

```
# Binary AI Video Classification - Requirements

# Core ML frameworks
torch>=2.2.0
torchvision>=0.17.0

# Hugging Face ecosystem
datasets>=2.14.0
huggingface-hub>=0.16.0
transformers>=4.30.0

# Data processing
numpy>=1.24.0
opencv-python>=4.8.0
Pillow>=9.5.0
imageio[ffmpeg]>=2.31.0

# Kaggle integration
kagglehub>=0.2.0

# Training utilities
tqdm>=4.65.0
scikit-learn>=1.3.0

# Environment management
python-dotenv>=1.0.0
```

### package.json

```
{
  "name": "artifact",
  "version": "1.0.0",
  "description": "A simple binary classifier to detect AI-generated videos using EfficientNet-V2-L and ConvNeXt-Base models with adaptive frame extraction.",
  "main": "index.js",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ArtiFACT-CalHacks/artifact.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "bugs": {
    "url": "https://github.com/ArtiFACT-CalHacks/artifact/issues"
  },
  "homepage": "https://github.com/ArtiFACT-CalHacks/artifact#readme",
  "dependencies": {
    "@types/react": "^19.2.2",
    "@types/react-dom": "^19.2.2",
    "@vitejs/plugin-react": "^5.1.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "typescript": "^5.9.3",
    "vite": "^7.1.12"
  }
}

```

### backend/requirements.txt

```
Flask==2.3.3
Flask-CORS==4.0.0
torch>=2.2.0
torchvision>=0.17.0
Pillow>=9.5.0
imageio[ffmpeg]>=2.31.0
numpy>=1.24.0
Werkzeug>=2.3.7

```

### src/main.tsx

```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import Detect from './pages/Detect.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Detect />
  </React.StrictMode>,
)

```

### backend/app.py

```python
from flask import Flask, request, jsonify
from flask_cors import CORS
import os
import uuid
import tempfile
from pathlib import Path
import logging
from werkzeug.utils import secure_filename

from model_loader import ModelLoader
from inference import InferenceEngine
from upload_handler import UploadHandler

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = Flask(__name__)
CORS(app)  # Enable CORS for frontend integration

# Configuration
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'mp4', 'avi', 'mov', 'mkv'}
MAX_FILE_SIZE = 100 * 1024 * 1024  # 100MB

# Initialize components
model_loader = ModelLoader()
inference_engine = InferenceEngine(model_loader)
upload_handler = UploadHandler(UPLOAD_FOLDER)

# Ensure upload directory exists
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/api/upload', methods=['POST'])
def upload_file():
    """Upload endpoint - accepts image/video files"""
    try:
        if 'file' not in request.files:
            return jsonify({
                'media_id': None,
                'success': False,
                'error': 'No file provided'
            }), 400
        
        file = request.files['file']
        if file.filename == '':
            return jsonify({
                'media_id': None,
                'success': False,
                'error': 'No file selected'
            }), 400
        
        if not allowed_file(file.filename):
            return jsonify({
                'media_id': None,
                'success': False,
                'error': 'File type not allowed'
            }), 400
        
        # Generate unique media ID
        media_id = str(uuid.uuid4())
        
        # Save file
        success = upload_handler.save_file(file, media_id)
        
        if success:
            logger.info(f"File uploaded successfully: {media_id}")
            return jsonify({
                'media_id': media_id,
                'success': True
            })
        else:
            return jsonify({
                'media_id': None,
                'success': False,
                'error': 'Failed to save file'
            }), 500
            
    except Exception as e:
        logger.error(f"Upload error: {str(e)}")
        return jsonify({
            'media_id': None,
            'success': False,
            'error': str(e)
        }), 500

@app.route('/api/detect', methods=['POST'])
def detect_authenticity():
    """Detection endpoint - analyzes uploaded file"""
    try:
        data = request.get_json()
        if not data or 'media_id' not in data:
            return jsonify({
                'is_ai': None,
                'confidence': None,
                'success': False,
                'error': 'media_id required'
            }), 400
        
        media_id = data['media_id']
        
        # Check if file exists
        file_path = upload_handler.get_file_path(media_id)
        if not file_path or not os.path.exists(file_path):
            return jsonify({
                'is_ai': None,
                'confidence': None,
                'success': False,
                'error': 'File not found'
            }), 404
        
        # Run inference
        result = inference_engine.predict(file_path)
        
        if result['success']:
            logger.info(f"Detection completed for {media_id}: {result['is_ai']} ({result['confidence']:.2f})")
            return jsonify({
                'is_ai': result['is_ai'],
                'confidence': result['confidence'],
                'success': True
            })
        else:
            return jsonify({
                'is_ai': None,
                'confidence': None,
                'success': False,
                'error': result['error']
            }), 500
            
    except Exception as e:
        logger.error(f"Detection error: {str(e)}")
        return jsonify({
            'is_ai': None,
            'confidence': None,
            'success': False,
            'error': str(e)
        }), 500

@app.route('/api/health', methods=['GET'])
def health_check():
    """Health check endpoint"""
    return jsonify({
        'status': 'healthy',
        'model_loaded': model_loader.is_loaded()
    })

if __name__ == '__main__':
    logger.info("Starting AI Detection API...")
    logger.info(f"Upload folder: {UPLOAD_FOLDER}")
    logger.info(f"Allowed extensions: {ALLOWED_EXTENSIONS}")
    
    # Load model on startup
    if model_loader.load_model():
        logger.info("✅ Model loaded successfully")
    else:
        logger.error("❌ Failed to load model")
    
    app.run(host='0.0.0.0', port=8000, debug=True)

```

### vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    host: true,
  },
})

```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AI Detection Tool</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### test_api.py

```python
#!/usr/bin/env python3
"""
Test script for the AI Detection Backend API
Tests all endpoints to ensure they're working correctly
"""

import requests
import json
import os
from pathlib import Path
from PIL import Image
import io

API_BASE_URL = "http://localhost:8000"

def create_test_image():
    """Create a proper test image"""
    # Create a simple test image
    img = Image.new('RGB', (100, 100), color='red')
    img_bytes = io.BytesIO()
    img.save(img_bytes, format='PNG')
    img_bytes.seek(0)
    return img_bytes.getvalue()

def test_health():
    """Test the health check endpoint"""
    print("🏥 Testing health endpoint...")
    try:
        response = requests.get(f"{API_BASE_URL}/api/health")
        if response.status_code == 200:
            data = response.json()
            print(f"✅ Health check passed: {data}")
            return data.get('model_loaded', False)
        else:
            print(f"❌ Health check failed: {response.status_code}")
            return False
    except Exception as e:
        print(f"❌ Health check error: {e}")
        return False

def test_upload():
    """Test file upload endpoint"""
    print("\n📤 Testing upload endpoint...")
    
    # Create a proper test image
    test_image_data = create_test_image()
    
    try:
        files = {'file': ('test.png', test_image_data, 'image/png')}
        response = requests.post(f"{API_BASE_URL}/api/upload", files=files)
        
        if response.status_code == 200:
            data = response.json()
            print(f"✅ Upload successful: {data}")
            return data.get('media_id')
        else:
            print(f"❌ Upload failed: {response.status_code} - {response.text}")
            return None
    except Exception as e:
        print(f"❌ Upload error: {e}")
        return None

def test_detection(media_id):
    """Test detection endpoint"""
    print(f"\n🔍 Testing detection endpoint with media_id: {media_id}")
    
    try:
        payload = {"media_id": media_id}
        response = requests.post(
            f"{API_BASE_URL}/api/detect",
            json=payload,
            headers={'Content-Type': 'application/json'}
        )
        
        if response.status_code == 200:
            data = response.json()
            print(f"✅ Detection successful: {data}")
            return True
        else:
            print(f"❌ Detection failed: {response.status_code} - {response.text}")
            return False
    except Exception as e:
        print(f"❌ Detection error: {e}")
        return False

def main():
    """Run all tests"""
    print("🚀 Starting API Tests")
    print("=" * 50)
    
    # Test 1: Health check
    model_loaded = test_health()
    if not model_loaded:
        print("❌ Model not loaded. Please check backend logs.")
        return
    
    # Test 2: Upload
    media_id = test_upload()
    if not media_id:
        print("❌ Upload test failed. Cannot proceed with detection test.")
        return
    
    # Test 3: Detection
    detection_success = test_detection(media_id)
    
    # Summary
    print("\n" + "=" * 50)
    print("📊 Test Summary:")
    print(f"✅ Health Check: {'PASSED' if model_loaded else 'FAILED'}")
    print(f"✅ Upload Test: {'PASSED' if media_id else 'FAILED'}")
    print(f"✅ Detection Test: {'PASSED' if detection_success else 'FAILED'}")
    
    if model_loaded and media_id and detection_success:
        print("\n🎉 All tests passed! Your API is working correctly.")
        print("\n🚀 Ready for frontend integration!")
    else:
        print("\n❌ Some tests failed. Please check the backend logs.")

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

### create_mock_dataset.py

```python
# -*- coding: utf-8 -*-
"""
Mock Dataset Creator - creates a small test dataset for training verification.

This creates a small dataset with synthetic videos to test the training pipeline
without needing the actual DeepAction dataset.
"""

import os
import argparse
import logging
from pathlib import Path
import numpy as np
from tqdm import tqdm
from PIL import Image
import torch
import torchvision.transforms as transforms
from torch.utils.data import Dataset, DataLoader
from typing import Tuple

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def create_mock_videos(output_dir: str, num_videos: int = 50):
    """
    Create mock video frames for testing.
    
    Args:
        output_dir: Directory to save mock frames
        num_videos: Number of mock videos to create
    """
    output_path = Path(output_dir)
    
    # Create output directories
    (output_path / "train" / "ai").mkdir(parents=True, exist_ok=True)
    (output_path / "train" / "real").mkdir(parents=True, exist_ok=True)
    (output_path / "val" / "ai").mkdir(parents=True, exist_ok=True)
    (output_path / "val" / "real").mkdir(parents=True, exist_ok=True)
    
    # Create mock videos
    for i in tqdm(range(num_videos), desc="Creating mock videos"):
        # Determine split and label
        split = "train" if i < num_videos * 0.8 else "val"
        label = "ai" if i % 2 == 0 else "real"
        
        # Create video directory
        video_dir = output_path / split / label / f"video_{i:04d}"
        video_dir.mkdir(parents=True, exist_ok=True)
        
        # Create 3-5 frames per video
        num_frames = np.random.randint(3, 6)
        
        for frame_idx in range(num_frames):
            # Create a random image
            if label == "ai":
                # AI-generated looking image (more structured patterns)
                img_array = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
                # Add some structured patterns
                img_array[50:150, 50:150] = [255, 0, 0]  # Red square
                img_array[100:200, 100:200] = [0, 255, 0]  # Green square
            else:
                # Real-looking image (more natural noise)
                img_array = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
                # Add some natural-looking patterns
                img_array[75:175, 75:175] = np.random.randint(100, 200, (100, 100, 3), dtype=np.uint8)
            
            # Save frame
            pil_image = Image.fromarray(img_array)
            frame_path = video_dir / f"{frame_idx:03d}.jpg"
            pil_image.save(frame_path, "JPEG", quality=95)
    
    logger.info(f"✅ Created {num_videos} mock videos")
    logger.info(f"Frames saved to: {output_dir}")


class FrameDataset(Dataset):
    """PyTorch dataset for loading pre-extracted frames."""
    
    def __init__(self, data_dir: str, split: str = "train", 
                 transform: transforms.Compose = None):
        """
        Initialize dataset.
        
        Args:
            data_dir: Path to data_frames directory
            split: Dataset split ("train" or "val")
            transform: Image transforms
        """
        self.data_dir = Path(data_dir) / split
        self.split = split
        
        # Default transforms
        if transform is None:
            self.transform = transforms.Compose([
                transforms.Resize((224, 224)),
                transforms.ToTensor(),
                transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                                   std=[0.229, 0.224, 0.225])
            ])
        else:
            self.transform = transform
        
        # Collect all video directories
        self.video_dirs = []
        self.labels = []
        
        for label in ["ai", "real"]:
            label_dir = self.data_dir / label
            if label_dir.exists():
                for video_dir in label_dir.iterdir():
                    if video_dir.is_dir():
                        self.video_dirs.append(video_dir)
                        self.labels.append(1 if label == "ai" else 0)
        
        logger.info(f"Loaded {len(self.video_dirs)} videos for {split} split")
        logger.info(f"  - AI videos: {sum(self.labels)}")
        logger.info(f"  - Real videos: {len(self.labels) - sum(self.labels)}")
    
    def __len__(self):
        return len(self.video_dirs)
    
    def __getitem__(self, idx):
        """Get a single video sample."""
        video_dir = self.video_dirs[idx]
        label = self.labels[idx]
        
        # Load all frames for this video
        frame_files = sorted(video_dir.glob("*.jpg"))
        frames = []
        
        for frame_file in frame_files:
            image = Image.open(frame_file).convert('RGB')
            tensor = self.transform(image)
            frames.append(tensor)
        
        # Stack frames: (T, C, H, W)
        frames_tensor = torch.stack(frames)
        
        return {
            'frames': frames_tensor,
            'label': torch.tensor(label, dtype=torch.float32),
            'video_id': video_dir.name
        }


def create_data_loaders(data_dir: str = "./data_frames",
                       batch_size: int = 8,
                       num_workers: int = 2) -> Tuple[DataLoader, DataLoader]:
    """Create train and validation data loaders."""
    
    # Create datasets
    train_dataset = FrameDataset(data_dir, split="train")
    val_dataset = FrameDataset(data_dir, split="val")
    
    # Create data loaders
    train_loader = DataLoader(
        train_dataset,
        batch_size=batch_size,
        shuffle=True,
        num_workers=num_workers,
        pin_memory=True,
        drop_last=True
    )
    
    val_loader = DataLoader(
        val_dataset,
        batch_size=batch_size,
        shuffle=False,
        num_workers=num_workers,
        pin_memory=True,
        drop_last=False
    )
    
    logger.info(f"Created data loaders - Tra
[truncated — 773 more characters]
```

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