diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..36eab36
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,54 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+*.egg-info/
+dist/
+build/
+*.egg
+
+# Virtual environments
+.venv/
+venv/
+ENV/
+env/
+
+# IDEs
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Git
+.git/
+.gitignore
+
+# Data and models (too large)
+data/
+checkpoints/*.pth
+raft_model/*.pth
+
+# Logs
+*.log
+logs/
+*.csv
+
+# OS
+.DS_Store
+Thumbs.db
+
+# UV
+.uv/
+
+# Backup files
+*.backup
+requirements.txt.backup
+
+# Documentation
+MIGRATION_SUMMARY.md
+
+# Demo videos (optional - comment out if needed)
+demo_video/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d394e4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,209 @@
+# Data directories (large datasets - should not be in repo)
+data/
+checkpoints/
+raft_model/
+
+# Environment variables (contains secrets)
+.env
+.env.example
+
+# Generated output folders
+frame/
+optical_result/
+results/
+logs/
+outputs/
+runs/
+
+# Demo video output
+demo_video/fake_sora/*.mp4
+demo_video/real/*.mp4
+# Keep the demo_video folder structure but ignore large video files
+!demo_video/fake_sora/.gitkeep
+!demo_video/real/.gitkeep
+
+# Model files and weights
+*.pth
+*.pt
+*.pkl
+*.ckpt
+*.h5
+*.hdf5
+*.pb
+*.onnx
+
+# Dataset files
+*.zip
+*.tar
+*.tar.gz
+*.rar
+*.7z
+
+# Video files
+*.mp4
+*.avi
+*.mov
+*.mkv
+*.wmv
+*.flv
+*.webm
+*.m4v
+
+# Image files (keep small docs images)
+*.png
+*.jpg
+*.jpeg
+*.bmp
+*.tiff
+*.tif
+*.gif
+# Allow images in specific documentation folders
+!fig/**/*.png
+!fig/**/*.jpg
+!fig/**/*.jpeg
+!docs/**/*.png
+!docs/**/*.jpg
+!docs/**/*.jpeg
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Virtual environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+.python-version
+
+# Jupyter Notebook
+.ipynb_checkpoints
+*.ipynb
+
+# PyCharm
+.idea/
+*.iml
+
+# VS Code
+.vscode/
+*.code-workspace
+
+# Sublime Text
+*.sublime-project
+*.sublime-workspace
+
+# Vim
+*.swp
+*.swo
+*~
+
+# Emacs
+*~
+\#*\#
+/.emacs.desktop
+/.emacs.desktop.lock
+*.elc
+auto-save-list
+tramp
+.\#*
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+Desktop.ini
+
+# Windows
+*.lnk
+
+# Temporary files
+*.tmp
+*.temp
+*.log
+*.bak
+*.backup
+*.swp
+*.cache
+
+# CUDA compilation cache
+.nv_cache/
+*.o
+
+# Experiment tracking
+wandb/
+tensorboard_logs/
+tb_logs/
+mlruns/
+
+# Large binary files
+*.bin
+*.dat
+*.raw
+
+# Coverage reports
+htmlcov/
+.coverage
+.coverage.*
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+.tox/
+nosetests.xml
+
+# Translations
+*.mo
+*.pot
+
+# Documentation builds
+docs/_build/
+docs/build/
+_build/
+.readthedocs.yml
+
+# Docker volumes
+.volumes/
+
+# Temporary extraction directories
+temp/
+tmp/
+
+# Configuration files with sensitive data
+config.json
+secrets.json
+.secrets
+credentials.json
+
+# Local development files
+.local/
+local_config.py
+dev_config.py
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..641602f
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.11.14
diff --git a/.streamlit/config.toml b/.streamlit/config.toml
new file mode 100644
index 0000000..3a5bd47
--- /dev/null
+++ b/.streamlit/config.toml
@@ -0,0 +1,5 @@
+[server]
+maxUploadSize = 500
+
+[browser]
+gatherUsageStats = false
diff --git a/Dockerfile.cpu b/Dockerfile.cpu
new file mode 100644
index 0000000..090da6a
--- /dev/null
+++ b/Dockerfile.cpu
@@ -0,0 +1,68 @@
+# Dockerfile for AIGVDet with CPU support
+FROM python:3.11-slim-bookworm
+
+# Set environment variables
+ENV PYTHONUNBUFFERED=1 \
+ PYTHONDONTWRITEBYTECODE=1 \
+ UV_SYSTEM_PYTHON=1
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+ git \
+ wget \
+ curl \
+ libglib2.0-0 \
+ libsm6 \
+ libxext6 \
+ libxrender-dev \
+ libgomp1 \
+ libgl1-mesa-glx \
+ unzip \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install uv
+RUN pip install --no-cache-dir uv
+
+# Set working directory
+WORKDIR /app
+
+# Copy project files
+COPY pyproject.toml ./
+COPY README.md ./
+COPY core/ ./core/
+COPY networks/ ./networks/
+COPY train.py test.py demo.py download_data.py ./
+
+# Create directories for data and checkpoints
+RUN mkdir -p /app/data /app/checkpoints /app/raft_model
+
+# Install other dependencies first
+RUN uv pip install --system \
+ einops \
+ imageio \
+ ipympl \
+ matplotlib \
+ natsort \
+ "numpy<2.0" \
+ opencv-python \
+ pandas \
+ scikit-learn \
+ tensorboard \
+ tensorboardX \
+ tqdm \
+ "blobfile>=1.0.5" \
+ wandb \
+ python-dotenv
+
+# Install PyTorch CPU version using pip directly
+RUN pip3 install torch==2.0.0+cpu torchvision==0.15.1+cpu \
+ --index-url https://download.pytorch.org/whl/cpu
+
+# Set Python path
+ENV PYTHONPATH=/app
+
+# Expose tensorboard port
+EXPOSE 6006
+
+# Default command
+CMD ["python", "train.py", "--help"]
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
new file mode 100644
index 0000000..daf0570
--- /dev/null
+++ b/Dockerfile.gpu-alt
@@ -0,0 +1,75 @@
+# Alternative GPU Dockerfile using PyTorch base image (avoids large download)
+FROM pytorch/pytorch:2.0.0-cuda11.7-cudnn8-runtime
+
+# Set environment variables
+ENV DEBIAN_FRONTEND=noninteractive \
+ PYTHONUNBUFFERED=1 \
+ PYTHONDONTWRITEBYTECODE=1 \
+ UV_SYSTEM_PYTHON=1
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+ git \
+ wget \
+ curl \
+ libglib2.0-0 \
+ libsm6 \
+ libxext6 \
+ libxrender-dev \
+ libgomp1 \
+ libgl1-mesa-glx \
+ unzip \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install uv
+RUN pip3 install --no-cache-dir uv
+
+# Set working directory
+WORKDIR /app
+
+# Copy project files
+COPY pyproject.toml ./
+COPY README.md ./
+COPY core/ ./core/
+COPY networks/ ./networks/
+COPY .streamlit/ ./.streamlit/
+COPY train.py test.py demo.py download_data.py gui_app.py ./
+
+# Create directories for data and checkpoints
+RUN mkdir -p /app/data /app/checkpoints /app/raft_model
+
+# Install dependencies (PyTorch already included in base image)
+RUN uv pip install --system \
+ einops \
+ imageio \
+ ipympl \
+ matplotlib \
+ natsort \
+ "numpy<2.0" \
+ opencv-python \
+ pandas \
+ scikit-learn \
+ tensorboard \
+ tensorboardX \
+ tqdm \
+ "blobfile>=1.0.5" \
+ "wandb==0.16.6" \
+ python-dotenv \
+ gdown
+
+# Install pyarrow with a version that has pre-built wheels, then streamlit
+RUN uv pip install --system "pyarrow>=14.0.0,<15.0.0" streamlit
+
+# Install torchvision separately (base image has torch but may need torchvision update)
+RUN pip3 install torchvision==0.15.1+cu117 \
+ --index-url https://download.pytorch.org/whl/cu117 || \
+ pip3 install torchvision==0.15.1
+
+# Set Python path
+ENV PYTHONPATH=/app
+
+# Expose tensorboard port
+EXPOSE 6006
+
+# Default command
+CMD ["/bin/bash"]
diff --git a/MIGRATION_SUMMARY.md b/MIGRATION_SUMMARY.md
new file mode 100644
index 0000000..799cef4
--- /dev/null
+++ b/MIGRATION_SUMMARY.md
@@ -0,0 +1,124 @@
+# Migration Summary: AIGVDet Project from requirements.txt to uv + pyproject.toml
+
+## Overview
+Successfully migrated the AIGVDet project from traditional pip/requirements.txt setup to modern uv package manager with pyproject.toml configuration.
+
+## Changes Made
+
+### 1. Installed uv Package Manager
+- Installed uv Python package manager for faster dependency resolution and project management
+
+### 2. Created pyproject.toml Configuration
+- Migrated all dependencies from `requirements.txt` to `pyproject.toml`
+- Added project metadata including:
+ - Project name: `aigvdet`
+ - Version: `0.1.0`
+ - Description: AI-Generated Video Detection via Spatial-Temporal Anomaly Learning
+ - Authors: Jianfa Bai, Man Lin, Gang Cao, Zijie Lou
+ - Python version requirement: `>=3.11, <3.12`
+
+### 3. Dependencies Migrated
+**Core Dependencies:**
+- torch==2.0.0+cu117 (with CUDA 11.7 support)
+- torchvision==0.15.1+cu117
+- einops
+- imageio
+- ipympl
+- matplotlib
+- numpy
+- opencv-python
+- pandas
+- scikit-learn
+- tensorboard
+- tensorboardX
+- tqdm
+- blobfile>=1.0.5
+- pip
+
+**Development Dependencies:**
+- hatchling (build backend)
+- setuptools
+- wheel
+
+### 4. PyTorch Configuration
+- Configured custom PyTorch index for CUDA support
+- Used PyTorch official wheel repository: https://download.pytorch.org/whl/cu117
+- Successfully installed PyTorch 2.0.0 with CUDA 11.7 support
+
+### 5. Build System Configuration
+- Set up hatchling as the build backend
+- Configured proper package structure for building
+- Added console scripts for easy execution:
+ - `aigvdet-train` → `train:main`
+ - `aigvdet-test` → `test:main`
+ - `aigvdet-demo` → `demo:main`
+
+### 6. Virtual Environment
+- Created Python 3.11.14 virtual environment
+- All dependencies successfully installed and tested
+- Project modules can be imported without issues
+
+### 7. Build Output
+- Successfully built both source distribution (`.tar.gz`) and wheel (`.whl`)
+- Files located in `dist/` directory:
+ - `aigvdet-0.1.0-py3-none-any.whl` (37 KB)
+ - `aigvdet-0.1.0.tar.gz` (27 MB)
+
+## Files Modified/Created
+- ✅ Created: `pyproject.toml` (main configuration)
+- ✅ Modified: `.python-version` (set to 3.11.14)
+- ✅ Created: `.venv/` (virtual environment)
+- ✅ Created: `dist/` (build outputs)
+- ✅ Backup: `requirements.txt.backup` (original requirements preserved)
+
+## Verification Tests
+- ✅ PyTorch imports successfully
+- ✅ PyTorch version: 2.0.0+cu117
+- ✅ All project dependencies import correctly
+- ✅ Core module imports without errors
+- ✅ Project builds successfully
+
+## How to Use
+
+### Installation
+```bash
+# Clone/navigate to project directory
+cd AIGVDet
+
+# Install all dependencies
+uv sync
+
+# Or install with development dependencies
+uv sync --group dev
+```
+
+### Running Scripts
+```bash
+# Activate the environment and run scripts
+uv run python train.py --gpus 0 --exp_name TRAIN_RGB_BRANCH
+
+# Or use console scripts (if configured)
+uv run aigvdet-train --gpus 0 --exp_name TRAIN_RGB_BRANCH
+```
+
+### Building
+```bash
+# Build source and wheel distributions
+uv build
+```
+
+## Benefits of Migration
+1. **Faster dependency resolution** - uv is significantly faster than pip
+2. **Better dependency management** - lockfile support and conflict resolution
+3. **Modern Python packaging** - follows PEP 621 standards
+4. **Reproducible builds** - exact dependency versions locked
+5. **Build system integration** - proper packaging with build backends
+6. **Development workflow** - better separation of runtime and dev dependencies
+
+## Notes
+- Original `requirements.txt` backed up as `requirements.txt.backup`
+- Python 3.11 is used for compatibility with PyTorch 2.0.0+cu117
+- CUDA availability depends on system configuration
+- All original functionality preserved
+
+The project is now ready for modern Python development workflows with uv!
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..dba7818
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,84 @@
+.PHONY: help build-gpu build-cpu build-all run-gpu run-cpu up-gpu up-cpu shell-gpu shell-cpu clean
+
+help:
+ @echo "AIGVDet Docker Makefile"
+ @echo ""
+ @echo "Build commands:"
+ @echo " make build-gpu - Build GPU Docker image"
+ @echo " make build-cpu - Build CPU Docker image"
+ @echo " make build-all - Build both images"
+ @echo ""
+ @echo "Run commands:"
+ @echo " make up-gpu - Start GPU container with docker-compose"
+ @echo " make up-cpu - Start CPU container with docker-compose"
+ @echo " make run-gpu - Run GPU container interactively"
+ @echo " make run-cpu - Run CPU container interactively"
+ @echo ""
+ @echo "Development:"
+ @echo " make shell-gpu - Open bash shell in GPU container"
+ @echo " make shell-cpu - Open bash shell in CPU container"
+ @echo ""
+ @echo "Cleanup:"
+ @echo " make clean - Remove containers and images"
+
+build-gpu:
+ docker build -f Dockerfile.gpu -t aigvdet:gpu .
+
+build-cpu:
+ docker build -f Dockerfile.cpu -t aigvdet:cpu .
+
+build-all: build-cpu build-gpu
+
+run-gpu:
+ docker run --gpus all -it --rm \
+ -v $(PWD)/data:/app/data \
+ -v $(PWD)/checkpoints:/app/checkpoints \
+ -v $(PWD)/raft_model:/app/raft_model \
+ -v $(PWD)/logs:/app/logs \
+ -p 6006:6006 \
+ aigvdet:gpu
+
+run-cpu:
+ docker run -it --rm \
+ -v $(PWD)/data:/app/data \
+ -v $(PWD)/checkpoints:/app/checkpoints \
+ -v $(PWD)/raft_model:/app/raft_model \
+ -v $(PWD)/logs:/app/logs \
+ -p 6006:6006 \
+ aigvdet:cpu
+
+up-gpu:
+ docker-compose up aigvdet-gpu
+
+up-cpu:
+ docker-compose up aigvdet-cpu
+
+shell-gpu:
+ docker run --gpus all -it --rm \
+ -v $(PWD)/data:/app/data \
+ -v $(PWD)/checkpoints:/app/checkpoints \
+ -v $(PWD)/raft_model:/app/raft_model \
+ aigvdet:gpu /bin/bash
+
+shell-cpu:
+ docker run -it --rm \
+ -v $(PWD)/data:/app/data \
+ -v $(PWD)/checkpoints:/app/checkpoints \
+ aigvdet:cpu /bin/bash
+
+clean:
+ docker-compose down
+ docker rmi aigvdet:gpu aigvdet:cpu || true
+
+train-gpu:
+ docker-compose run aigvdet-gpu python3.11 train.py --gpus 0 --exp_name default_exp
+
+train-cpu:
+ docker-compose run aigvdet-cpu python train.py --exp_name default_exp
+
+tensorboard:
+ docker run --gpus all -it --rm \
+ -v $(PWD)/logs:/app/logs \
+ -p 6006:6006 \
+ aigvdet:gpu \
+ tensorboard --logdir=/app/logs --host=0.0.0.0 --port=6006
diff --git a/README.md b/README.md
index b3eb259..f343623 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,19 @@
## AIGVDet
An official implementation code for paper "AI-Generated Video Detection via Spatial-Temporal Anomaly Learning", PRCV 2024. This repo will provide codes, trained weights, and our training datasets.
+## 🐳 Docker Support
+Now supports Docker with both CPU and GPU versions! See [DOCKER_QUICKREF.md](DOCKER_QUICKREF.md) for quick commands or [DOCKER_USAGE.md](DOCKER_USAGE.md) for detailed instructions.
+
+**Quick Start with Docker:**
+```bash
+# Build and run GPU version
+docker-compose up --build aigvdet-gpu
+
+# Or build manually
+./build-docker.ps1 gpu # Windows
+./build-docker.sh gpu # Linux/Mac
+```
+
## Network Architecture
diff --git a/batch_prepare.py b/batch_prepare.py
new file mode 100644
index 0000000..11485f3
--- /dev/null
+++ b/batch_prepare.py
@@ -0,0 +1,48 @@
+"""
+Batch Data Preparation Script
+Automatically finds all *_mp4 folders in data/test/T2V and processes them.
+"""
+import os
+import glob
+import subprocess
+from pathlib import Path
+
+# Configuration
+SOURCE_ROOT = "data/test/T2V"
+RGB_OUTPUT_ROOT = "data/test/original/T2V"
+FLOW_OUTPUT_ROOT = "data/test/T2V"
+RAFT_MODEL = "raft-model/raft-things.pth"
+MAX_VIDEOS = 100 # Limit to 100 videos per dataset for faster processing
+
+def main():
+ # Find all folders ending in _mp4
+ source_folders = glob.glob(os.path.join(SOURCE_ROOT, "*_mp4"))
+
+ print(f"Found {len(source_folders)} datasets to process: {[os.path.basename(f) for f in source_folders]}")
+
+ for source_dir in source_folders:
+ dataset_name = os.path.basename(source_dir).replace("_mp4", "")
+
+ print(f"\n{'='*60}")
+ print(f"Processing: {dataset_name}")
+ print(f"{'='*60}")
+
+ # Define output paths
+ rgb_out = os.path.join(RGB_OUTPUT_ROOT, dataset_name)
+ flow_out = os.path.join(FLOW_OUTPUT_ROOT, dataset_name)
+
+ cmd = [
+ "python", "prepare_data.py",
+ "--source_dir", source_dir,
+ "--output_rgb_dir", rgb_out,
+ "--output_flow_dir", flow_out,
+ "--model", RAFT_MODEL,
+ "--label", "1_fake", # Assuming these are all generated video folders
+ "--max_videos", str(MAX_VIDEOS)
+ ]
+
+ print(f"Command: {' '.join(cmd)}")
+ subprocess.run(cmd)
+
+if __name__ == "__main__":
+ main()
diff --git a/batch_prepare_all.py b/batch_prepare_all.py
new file mode 100644
index 0000000..fa212fb
--- /dev/null
+++ b/batch_prepare_all.py
@@ -0,0 +1,200 @@
+"""
+Batch Data Preparation Script for All T2V Models
+Automatically finds all *_mp4 folders in data/test/T2V and processes them.
+"""
+import os
+import glob
+import cv2
+import numpy as np
+import torch
+import sys
+from PIL import Image
+from tqdm import tqdm
+from pathlib import Path
+
+# Add core to path for RAFT
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+# Configuration
+SOURCE_ROOT = "data/test/T2V"
+RGB_OUTPUT_ROOT = "data/test/original/T2V"
+FLOW_OUTPUT_ROOT = "data/test/T2V"
+RAFT_MODEL = "raft_model/raft-things.pth"
+MAX_VIDEOS = 500 # Limit to 500 videos per dataset
+
+def load_image(imfile):
+ """Load image and convert to tensor"""
+ img = Image.open(imfile)
+
+ # Resize if too large to prevent OOM
+ max_dim = 1024
+ if max(img.size) > max_dim:
+ scale = max_dim / max(img.size)
+ new_size = (int(img.size[0] * scale), int(img.size[1] * scale))
+ img = img.resize(new_size, Image.BILINEAR)
+
+ img = np.array(img).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def save_flow(img, flo, output_path):
+ """Save optical flow as image"""
+ img = img[0].permute(1, 2, 0).cpu().numpy()
+ flo = flo[0].permute(1, 2, 0).cpu().numpy()
+ flo = flow_viz.flow_to_image(flo)
+ cv2.imwrite(output_path, flo)
+
+def extract_frames(video_path, output_folder, max_frames=95):
+ """Extract frames from video (limit to max_frames)"""
+ os.makedirs(output_folder, exist_ok=True)
+
+ # Check if already extracted
+ existing = glob.glob(os.path.join(output_folder, "*.png"))
+ if len(existing) > 0:
+ return sorted(existing)[:max_frames]
+
+ cap = cv2.VideoCapture(video_path)
+ frame_count = 0
+ frames = []
+
+ while cap.isOpened() and frame_count < max_frames:
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ frame_path = os.path.join(output_folder, f"{frame_count:08d}.png")
+ cv2.imwrite(frame_path, frame)
+ frames.append(frame_path)
+ frame_count += 1
+
+ cap.release()
+ return frames
+
+def generate_optical_flow(model, frames, output_dir):
+ """Generate optical flow for frame sequence"""
+ os.makedirs(output_dir, exist_ok=True)
+
+ with torch.no_grad():
+ for i, (imfile1, imfile2) in enumerate(tqdm(zip(frames[:-1], frames[1:]),
+ total=len(frames)-1,
+ desc=" Generating flow")):
+ flow_path = os.path.join(output_dir, f"{i:08d}.png")
+
+ if os.path.exists(flow_path):
+ continue
+
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+ save_flow(image1, flow_up, flow_path)
+
+def process_dataset(dataset_name, source_dir, model):
+ """Process a single T2V dataset"""
+ print(f"\n{'='*60}")
+ print(f"Processing: {dataset_name}")
+ print(f"{'='*60}")
+
+ # Get all videos
+ videos = glob.glob(os.path.join(source_dir, "*.mp4")) + \
+ glob.glob(os.path.join(source_dir, "*.avi")) + \
+ glob.glob(os.path.join(source_dir, "*.mov"))
+
+ # Apply limit
+ if len(videos) > MAX_VIDEOS:
+ videos = videos[:MAX_VIDEOS]
+ print(f"Limited to {MAX_VIDEOS} videos")
+
+ print(f"Found {len(videos)} videos to process")
+
+ # Output directories
+ rgb_base = os.path.join(RGB_OUTPUT_ROOT, dataset_name, "1_fake")
+ flow_base = os.path.join(FLOW_OUTPUT_ROOT, dataset_name, "1_fake")
+
+ # Process each video
+ for idx, video_path in enumerate(videos, 1):
+ video_name = Path(video_path).stem
+ print(f"\n[{idx}/{len(videos)}] {video_name}")
+
+ rgb_out = os.path.join(rgb_base, video_name)
+ flow_out = os.path.join(flow_base, video_name)
+
+ # Extract frames
+ print(" Extracting frames...")
+ frames = extract_frames(video_path, rgb_out)
+
+ if len(frames) < 2:
+ print(" ⚠ Skipping (too few frames)")
+ continue
+
+ # Generate optical flow
+ generate_optical_flow(model, frames, flow_out)
+
+ print(f"\n✅ Completed {dataset_name}")
+
+def main():
+ # Find all T2V dataset folders
+ source_folders = glob.glob(os.path.join(SOURCE_ROOT, "*_mp4"))
+
+ if not source_folders:
+ print(f"❌ No *_mp4 folders found in {SOURCE_ROOT}")
+ return
+
+ print("="*60)
+ print("BATCH T2V DATA PREPARATION")
+ print("="*60)
+ print(f"\nFound {len(source_folders)} dataset(s):")
+ for folder in source_folders:
+ dataset_name = os.path.basename(folder).replace("_mp4", "")
+ print(f" 📁 {dataset_name}")
+
+ print(f"\nConfiguration:")
+ print(f" • Max videos per dataset: {MAX_VIDEOS}")
+ print(f" • Max frames per video: 95 (RGB) / 94 (Flow)")
+ print(f" • RAFT model: {RAFT_MODEL}")
+ print("="*60)
+
+ # Load RAFT model
+ print("\nLoading RAFT model...")
+ import argparse
+ raft_args = argparse.Namespace(
+ model=RAFT_MODEL,
+ small=False,
+ mixed_precision=False,
+ alternate_corr=False,
+ dropout=0
+ )
+
+ model = torch.nn.DataParallel(RAFT(raft_args))
+ model.load_state_dict(torch.load(RAFT_MODEL, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+ print("✓ RAFT model loaded")
+
+ # Process each dataset
+ for idx, source_dir in enumerate(source_folders, 1):
+ dataset_name = os.path.basename(source_dir).replace("_mp4", "")
+
+ try:
+ process_dataset(dataset_name, source_dir, model)
+ except Exception as e:
+ print(f"\n❌ Error processing {dataset_name}: {e}")
+ response = input("Continue? (Y/n): ").strip().lower()
+ if response == 'n':
+ break
+
+ print("\n" + "="*60)
+ print("✅ BATCH PROCESSING COMPLETE!")
+ print("="*60)
+
+if __name__ == "__main__":
+ main()
diff --git a/batch_prepare_i2v.py b/batch_prepare_i2v.py
new file mode 100644
index 0000000..d97ba8a
--- /dev/null
+++ b/batch_prepare_i2v.py
@@ -0,0 +1,199 @@
+"""
+Batch Data Preparation Script for I2V Models
+Processes all *_mp4 folders in data/I2V
+"""
+import os
+import glob
+import cv2
+import numpy as np
+import torch
+import sys
+from PIL import Image
+from tqdm import tqdm
+from pathlib import Path
+
+# Add core to path for RAFT
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+# Configuration
+SOURCE_ROOT = "data/I2V"
+RGB_OUTPUT_ROOT = "data/test/original/I2V"
+FLOW_OUTPUT_ROOT = "data/test/I2V"
+RAFT_MODEL = "raft_model/raft-things.pth"
+MAX_VIDEOS = 200 # Limit to 200 videos per dataset
+
+def load_image(imfile):
+ """Load image and convert to tensor"""
+ img = Image.open(imfile)
+
+ # Resize if too large to prevent OOM
+ max_dim = 1024
+ if max(img.size) > max_dim:
+ scale = max_dim / max(img.size)
+ new_size = (int(img.size[0] * scale), int(img.size[1] * scale))
+ img = img.resize(new_size, Image.BILINEAR)
+
+ img = np.array(img).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def save_flow(img, flo, output_path):
+ """Save optical flow as image"""
+ img = img[0].permute(1, 2, 0).cpu().numpy()
+ flo = flo[0].permute(1, 2, 0).cpu().numpy()
+ flo = flow_viz.flow_to_image(flo)
+ cv2.imwrite(output_path, flo)
+
+def extract_frames(video_path, output_folder, max_frames=95):
+ """Extract frames from video (limit to max_frames)"""
+ os.makedirs(output_folder, exist_ok=True)
+
+ # Check if already extracted
+ existing = glob.glob(os.path.join(output_folder, "*.png"))
+ if len(existing) > 0:
+ return sorted(existing)[:max_frames] # Return only up to max_frames
+
+ cap = cv2.VideoCapture(video_path)
+ frame_count = 0
+ frames = []
+
+ while cap.isOpened() and frame_count < max_frames:
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ frame_path = os.path.join(output_folder, f"{frame_count:08d}.png")
+ cv2.imwrite(frame_path, frame)
+ frames.append(frame_path)
+ frame_count += 1
+
+ cap.release()
+ return frames
+
+def generate_optical_flow(model, frames, output_dir):
+ """Generate optical flow for frame sequence"""
+ os.makedirs(output_dir, exist_ok=True)
+
+ with torch.no_grad():
+ for i, (imfile1, imfile2) in enumerate(tqdm(zip(frames[:-1], frames[1:]),
+ total=len(frames)-1,
+ desc=" Generating flow")):
+ flow_path = os.path.join(output_dir, f"{i:08d}.png")
+
+ if os.path.exists(flow_path):
+ continue
+
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+ save_flow(image1, flow_up, flow_path)
+
+def process_dataset(dataset_name, source_dir, model):
+ """Process a single I2V dataset"""
+ print(f"\n{'='*60}")
+ print(f"Processing: {dataset_name}")
+ print(f"{'='*60}")
+
+ # Get all videos
+ videos = glob.glob(os.path.join(source_dir, "*.mp4")) + \
+ glob.glob(os.path.join(source_dir, "*.avi")) + \
+ glob.glob(os.path.join(source_dir, "*.mov"))
+
+ # Apply limit
+ if len(videos) > MAX_VIDEOS:
+ videos = videos[:MAX_VIDEOS]
+ print(f"Limited to {MAX_VIDEOS} videos")
+
+ print(f"Found {len(videos)} videos to process")
+
+ # Output directories
+ rgb_base = os.path.join(RGB_OUTPUT_ROOT, dataset_name, "1_fake")
+ flow_base = os.path.join(FLOW_OUTPUT_ROOT, dataset_name, "1_fake")
+
+ # Process each video
+ for idx, video_path in enumerate(videos, 1):
+ video_name = Path(video_path).stem
+ print(f"\n[{idx}/{len(videos)}] {video_name}")
+
+ rgb_out = os.path.join(rgb_base, video_name)
+ flow_out = os.path.join(flow_base, video_name)
+
+ # Extract frames
+ print(" Extracting frames...")
+ frames = extract_frames(video_path, rgb_out)
+
+ if len(frames) < 2:
+ print(" ⚠ Skipping (too few frames)")
+ continue
+
+ # Generate optical flow
+ generate_optical_flow(model, frames, flow_out)
+
+ print(f"\n✅ Completed {dataset_name}")
+
+def main():
+ # Find all I2V dataset folders
+ source_folders = glob.glob(os.path.join(SOURCE_ROOT, "*_mp4"))
+
+ if not source_folders:
+ print(f"❌ No *_mp4 folders found in {SOURCE_ROOT}")
+ return
+
+ print("="*60)
+ print("BATCH I2V DATA PREPARATION")
+ print("="*60)
+ print(f"\nFound {len(source_folders)} dataset(s):")
+ for folder in source_folders:
+ dataset_name = os.path.basename(folder).replace("_mp4", "")
+ print(f" 📁 {dataset_name}")
+
+ print(f"\nConfiguration:")
+ print(f" • Max videos per dataset: {MAX_VIDEOS}")
+ print(f" • RAFT model: {RAFT_MODEL}")
+ print("="*60)
+
+ # Load RAFT model
+ print("\nLoading RAFT model...")
+ import argparse
+ raft_args = argparse.Namespace(
+ model=RAFT_MODEL,
+ small=False,
+ mixed_precision=False,
+ alternate_corr=False,
+ dropout=0
+ )
+
+ model = torch.nn.DataParallel(RAFT(raft_args))
+ model.load_state_dict(torch.load(RAFT_MODEL, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+ print("✓ RAFT model loaded")
+
+ # Process each dataset
+ for idx, source_dir in enumerate(source_folders, 1):
+ dataset_name = os.path.basename(source_dir).replace("_mp4", "")
+
+ try:
+ process_dataset(dataset_name, source_dir, model)
+ except Exception as e:
+ print(f"\n❌ Error processing {dataset_name}: {e}")
+ response = input("Continue? (Y/n): ").strip().lower()
+ if response == 'n':
+ break
+
+ print("\n" + "="*60)
+ print("✅ BATCH PROCESSING COMPLETE!")
+ print("="*60)
+
+if __name__ == "__main__":
+ main()
diff --git a/clean_extra_frames.py b/clean_extra_frames.py
new file mode 100644
index 0000000..8109291
--- /dev/null
+++ b/clean_extra_frames.py
@@ -0,0 +1,81 @@
+import os
+import glob
+import argparse
+
+def clean_extra_frames(base_dir, max_frame_index=94):
+ print(f"Cleaning frames with index > {max_frame_index} in {base_dir}")
+
+ # Datasets to check
+ datasets = ["moonvalley", "videocraft", "pika", "neverends"]
+
+ total_deleted = 0
+
+ for dataset in datasets:
+ target_dir = os.path.join(base_dir, dataset, "0_real")
+
+ if not os.path.exists(target_dir):
+ print(f"Skipping {target_dir} (not found)")
+ continue
+
+ print(f"Scanning {target_dir}...")
+
+ # Check if it's flat files or folders
+ # Based on previous ls, it seems to be flat files for 0_real in some contexts,
+ # but prepare_real_data.py copies folders: shutil.copytree(temp_flow_dir, flow_dest)
+ # Let's handle both cases.
+
+ items = os.listdir(target_dir)
+ for item in items:
+ item_path = os.path.join(target_dir, item)
+
+ if os.path.isdir(item_path):
+ # It's a video folder
+ video_name = item
+ frames = glob.glob(os.path.join(item_path, "*"))
+ for frame in frames:
+ if should_delete(frame, max_frame_index):
+ os.remove(frame)
+ total_deleted += 1
+ else:
+ # It's a flat file
+ if should_delete(item_path, max_frame_index):
+ os.remove(item_path)
+ total_deleted += 1
+
+ print(f"\n✓ Cleanup complete. Deleted {total_deleted} extra frames.")
+
+def should_delete(filepath, max_index):
+ filename = os.path.basename(filepath)
+ name_part = os.path.splitext(filename)[0]
+
+ # Case 1: Filename is just a number (e.g. 00000094.jpg)
+ if name_part.isdigit():
+ idx = int(name_part)
+ if idx > 90:
+ print(f" Checking {filename}: Index {idx} > {max_index}? {idx > max_index}")
+ if idx > max_index:
+ return True
+
+ # Case 2: Filename has underscores (e.g. video_00000094.jpg)
+ parts = name_part.split('_')
+ if len(parts) > 1:
+ last_part = parts[-1]
+ if last_part.isdigit():
+ idx = int(last_part)
+ if idx > 90:
+ print(f" Checking {filename}: Index {idx} > {max_index}? {idx > max_index}")
+ if idx > max_index:
+ return True
+
+ return False
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--dry-run", action="store_true", help="Print what would be deleted without deleting")
+ args = parser.parse_args()
+
+ # Optical Flow path (Limit to 94 frames -> max index 93)
+ clean_extra_frames("data/test/T2V", max_frame_index=93)
+
+ # RGB path (Limit to 95 frames -> max index 94)
+ clean_extra_frames("data/test/original/T2V", max_frame_index=94)
diff --git a/compile_table2_data2.py b/compile_table2_data2.py
new file mode 100644
index 0000000..b0b8b19
--- /dev/null
+++ b/compile_table2_data2.py
@@ -0,0 +1,70 @@
+ """
+ Quick script to compile Table 2 from existing CSV files in data2/results/table2/
+ """
+
+ import pandas as pd
+ from pathlib import Path
+ from sklearn.metrics import accuracy_score, roc_auc_score, average_precision_score
+
+ RESULTS_DIR = Path("data2/results/table2")
+
+ DATASETS = ["emu", "hotshot", "sora"]
+ VARIANTS = ["S_spatial", "S_optical", "AIGVDet"]
+
+ def get_metrics_from_csv(csv_path):
+ """Calculate metrics from CSV file"""
+ try:
+ df = pd.read_csv(csv_path)
+
+ if len(df) == 0:
+ return None
+
+ y_true = df['flag'].values
+ y_pred = df['pro'].values
+
+ acc = accuracy_score(y_true, (y_pred >= 0.5).astype(int))
+ auc = roc_auc_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+ ap = average_precision_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+
+ return {'ACC': acc, 'AUC': auc, 'AP': ap}
+ except Exception as e:
+ print(f"Error reading {csv_path}: {e}")
+ return None
+
+ # Compile results
+ rows = []
+ for dataset in DATASETS:
+ row = {"Dataset": dataset}
+
+ for variant in VARIANTS:
+ csv_file = RESULTS_DIR / f"{dataset}_{variant}_video.csv"
+
+ if csv_file.exists():
+ metrics = get_metrics_from_csv(csv_file)
+ if metrics:
+ acc = metrics['ACC'] * 100
+ auc = metrics['AUC'] * 100
+ row[variant] = f"{acc:.1f}/{auc:.1f}"
+ print(f"✓ {dataset} - {variant}: ACC={acc:.1f}%, AUC={auc:.1f}%")
+ else:
+ row[variant] = "N/A"
+ print(f"✗ {dataset} - {variant}: No data")
+ else:
+ row[variant] = "N/A"
+ print(f"✗ {dataset} - {variant}: File not found")
+
+ rows.append(row)
+
+ # Create and save table
+ df = pd.DataFrame(rows)
+
+ output_file = RESULTS_DIR / "table2_data2_compiled.csv"
+ df.to_csv(output_file, index=False)
+
+ print("\n" + "="*60)
+ print("TABLE 2 (DATA2)")
+ print("="*60)
+ print(df.to_string(index=False))
+ print("\n" + "="*60)
+ print(f"✓ Saved to: {output_file}")
+ print("="*60)
diff --git a/compile_table2_data3.py b/compile_table2_data3.py
new file mode 100644
index 0000000..b3656c2
--- /dev/null
+++ b/compile_table2_data3.py
@@ -0,0 +1,70 @@
+"""
+Quick script to compile Table 2 from existing CSV files in data3/results/table2/
+"""
+
+import pandas as pd
+from pathlib import Path
+from sklearn.metrics import accuracy_score, roc_auc_score, average_precision_score
+
+RESULTS_DIR = Path("data3/results/table2")
+
+DATASETS = ["moonvalley", "pika", "neverends"]
+VARIANTS = ["S_spatial", "S_optical", "AIGVDet"]
+
+def get_metrics_from_csv(csv_path):
+ """Calculate metrics from CSV file"""
+ try:
+ df = pd.read_csv(csv_path)
+
+ if len(df) == 0:
+ return None
+
+ y_true = df['flag'].values
+ y_pred = df['pro'].values
+
+ acc = accuracy_score(y_true, (y_pred >= 0.5).astype(int))
+ auc = roc_auc_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+ ap = average_precision_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+
+ return {'ACC': acc, 'AUC': auc, 'AP': ap}
+ except Exception as e:
+ print(f"Error reading {csv_path}: {e}")
+ return None
+
+# Compile results
+rows = []
+for dataset in DATASETS:
+ row = {"Dataset": dataset}
+
+ for variant in VARIANTS:
+ csv_file = RESULTS_DIR / f"{dataset}_{variant}_video.csv"
+
+ if csv_file.exists():
+ metrics = get_metrics_from_csv(csv_file)
+ if metrics:
+ acc = metrics['ACC'] * 100
+ auc = metrics['AUC'] * 100
+ row[variant] = f"{acc:.1f}/{auc:.1f}"
+ print(f"✓ {dataset} - {variant}: ACC={acc:.1f}%, AUC={auc:.1f}%")
+ else:
+ row[variant] = "N/A"
+ print(f"✗ {dataset} - {variant}: No data")
+ else:
+ row[variant] = "N/A"
+ print(f"✗ {dataset} - {variant}: File not found")
+
+ rows.append(row)
+
+# Create and save table
+df = pd.DataFrame(rows)
+
+output_file = RESULTS_DIR / "table2_data3_i2v_compiled.csv"
+df.to_csv(output_file, index=False)
+
+print("\n" + "="*60)
+print("TABLE 2 (DATA3 - I2V)")
+print("="*60)
+print(df.to_string(index=False))
+print("\n" + "="*60)
+print(f"✓ Saved to: {output_file}")
+print("="*60)
diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..3953acc
Binary files /dev/null and b/core/__pycache__/__init__.cpython-311.pyc differ
diff --git a/core/utils1/config.py b/core/utils1/config.py
index 1abb3c6..96cf963 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -10,48 +10,51 @@ class DefaultConfigs(ABC):
gpus = [0]
seed = 3407
arch = "resnet50"
- datasets = ["zhaolian_train"]
- datasets_test = ["adm_res_abs_ddim20s"]
+ datasets = ["trainset_1"] # Changed from trainset_1 to match test/train structure
+ datasets_test = ["val_set_1"] # Changed from val_set_1 to match test/val structure
mode = "binary"
class_bal = False
- batch_size = 64
- loadSize = 256
- cropSize = 224
+ batch_size = 64 # RTX 3090 24GB can handle original batch size
+ loadSize = 512 # Resizes the image, this is used as it is standard for HIGH RES CNNs and must have margin before cropping
+ cropSize = 448
epoch = "latest"
- num_workers = 20
+ num_workers = 16 # Increased for faster data loading on 24GB GPU (choose 8 or 16) - doesnt affect accuracy but only throughput
serial_batches = False
isTrain = True
- # data augmentation
+ # data augmentation - to match the paper's augmentation rate (10%), set blur and jpg to 0.5
rz_interp = ["bilinear"]
- # blur_prob = 0.0
- blur_prob = 0.1
+ # blur_prob = 0.1 - resnet50 template
+ blur_prob = 0.1 # optical flow
blur_sig = [0.5]
- # jpg_prob = 0.0
- jpg_prob = 0.1
+ # jpg_prob = 0.1 - resnet50 template
+ jpg_prob = 0.1 # optical flow
+ # P(augmented) = 1-(1-0.5)(1-0.05) = 0.975
jpg_method = ["cv2"]
- jpg_qual = [75]
+ jpg_qual = list(range(70, 91))
gray_prob = 0.0
aug_resize = True
- aug_crop = True
- aug_flip = True
- aug_norm = True
+ aug_crop = True
+ aug_flip = True # optical flow
+ aug_norm = True # optical flow
####### train setting ######
warmup = False
# warmup = True
warmup_epoch = 3
+ # earlystop = True - resnet50 template, training ends only when lr reaches 1e-6 which trainer already supports (Trainer.adjust_learning_rate(min_lr=1e-6))
earlystop = True
earlystop_epoch = 5
optim = "adam"
new_optim = False
loss_freq = 400
save_latest_freq = 2000
- save_epoch_freq = 20
+ save_epoch_freq = 5
continue_train = False
epoch_count = 1
last_epoch = -1
- nepoch = 400
+ # nepoch = 100, try
+ nepoch = 50
beta1 = 0.9
lr = 0.0001
init_type = "normal"
@@ -59,7 +62,7 @@ class DefaultConfigs(ABC):
pretrained = True
# paths information
- root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+ root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
dataset_root = os.path.join(root_dir, "data")
exp_root = os.path.join(root_dir, "data", "exp")
_exp_name = ""
diff --git a/core/utils1/datasets.py b/core/utils1/datasets.py
index 4a150de..cac43e8 100644
--- a/core/utils1/datasets.py
+++ b/core/utils1/datasets.py
@@ -13,7 +13,7 @@
from scipy.ndimage import gaussian_filter
from torch.utils.data.sampler import WeightedRandomSampler
-from utils1.config import CONFIGCLASS
+from core.utils1.config import CONFIGCLASS
ImageFile.LOAD_TRUNCATED_IMAGES = True
@@ -27,37 +27,46 @@ def dataset_folder(root: str, cfg: CONFIGCLASS):
def binary_dataset(root: str, cfg: CONFIGCLASS):
+ # identity_transform = transforms.Lambda(lambda img: img)
+ # rz_func = identity_transform
+ # issue here, destroys performance as no resizing happens at all that go straight to cropping even if they are different resolutions
+
identity_transform = transforms.Lambda(lambda img: img)
-
- rz_func = identity_transform
-
+
+ # Enable resize (paper implies resize > crop for random cropping)
+ if cfg.aug_resize:
+ rz_func = transforms.Lambda(lambda img: custom_resize(img, cfg))
+ else:
+ rz_func = identity_transform
+
+ # Crop to cfg.cropSize (paper uses 448)
if cfg.isTrain:
- crop_func = transforms.RandomCrop((448,448))
+ crop_func = transforms.RandomCrop((cfg.cropSize, cfg.cropSize))
else:
- crop_func = transforms.CenterCrop((448,448)) if cfg.aug_crop else identity_transform
+ crop_func = transforms.CenterCrop((cfg.cropSize, cfg.cropSize)) if cfg.aug_crop else identity_transform
+ # Flip only in training if enabled (disabled for optical flow)
if cfg.isTrain and cfg.aug_flip:
flip_func = transforms.RandomHorizontalFlip()
else:
- flip_func = identity_transform
-
+ flip_func = identity_transform # fallback
+
return datasets.ImageFolder(
root,
transforms.Compose(
[
- rz_func,
- #change
- transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
- crop_func,
- flip_func,
- transforms.ToTensor(),
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
- if cfg.aug_norm
- else identity_transform,
- ]
+ rz_func,
+ transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)), #change
+ crop_func,
+ flip_func,
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ if cfg.aug_norm
+ else identity_transform,
+ ]
+ )
)
- )
class FileNameDataset(datasets.ImageFolder):
@@ -139,11 +148,11 @@ def jpeg_from_key(img: np.ndarray, compress_val: int, key: str) -> np.ndarray:
'bicubic': Image.BICUBIC,
'lanczos': Image.LANCZOS,
'nearest': Image.NEAREST}
-def custom_resize(img: Image.Image, cfg: CONFIGCLASS) -> Image.Image:
+
+def custom_resize(img: Image.Image, cfg: CONFIGCLASS) -> Image.Image: # added to implement 70 to 90
interp = sample_discrete(cfg.rz_interp)
return TF.resize(img, cfg.loadSize, interpolation=rz_dict[interp])
-
def get_dataset(cfg: CONFIGCLASS):
dset_lst = []
for dataset in cfg.datasets:
diff --git a/core/utils1/earlystop.py b/core/utils1/earlystop.py
index 741d07e..3fe8b50 100644
--- a/core/utils1/earlystop.py
+++ b/core/utils1/earlystop.py
@@ -1,6 +1,7 @@
import numpy as np
+import wandb
-from utils1.trainer import Trainer
+from core.utils1.trainer import Trainer
class EarlyStopping:
@@ -41,6 +42,6 @@ def __call__(self, score: float, trainer: Trainer):
def save_checkpoint(self, score: float, trainer: Trainer):
"""Saves model when validation loss decrease."""
if self.verbose:
- print(f"Validation accuracy increased ({self.score_max:.6f} --> {score:.6f}). Saving model ...")
- trainer.save_networks("best")
- self.score_max = score
+ print(f"Validation accuracy increased ({self.score_max:.6f} --> {score:.6f}).")
+ # trainer.save_networks("best") - Handled globally in train.py to avoid overwriting with local bests
+ self.score_max = score
diff --git a/core/utils1/eval.py b/core/utils1/eval.py
index eaf62c2..d896977 100644
--- a/core/utils1/eval.py
+++ b/core/utils1/eval.py
@@ -6,8 +6,8 @@
import torch
import torch.nn as nn
-from utils1.config import CONFIGCLASS
-from utils1.utils import to_cuda
+from core.utils1.config import CONFIGCLASS
+from core.utils1.utils import to_cuda
def get_val_cfg(cfg: CONFIGCLASS, split="val", copy=True):
@@ -37,7 +37,7 @@ def get_val_cfg(cfg: CONFIGCLASS, split="val", copy=True):
def validate(model: nn.Module, cfg: CONFIGCLASS):
from sklearn.metrics import accuracy_score, average_precision_score, roc_auc_score
- from utils1.datasets import create_dataloader
+ from core.utils1.datasets import create_dataloader
data_loader = create_dataloader(cfg)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -57,10 +57,21 @@ def validate(model: nn.Module, cfg: CONFIGCLASS):
f_acc = accuracy_score(y_true[y_true == 1], y_pred[y_true == 1] > 0.5)
acc = accuracy_score(y_true, y_pred > 0.5)
ap = average_precision_score(y_true, y_pred)
+ auc = roc_auc_score(y_true, y_pred)
+
+ # Calculate TPR (True Positive Rate / Recall) and TNR (True Negative Rate / Specificity)
+ tpr = f_acc # TPR is the accuracy on fake samples (class 1)
+ tnr = r_acc # TNR is the accuracy on real samples (class 0)
+
+
+ # added values for results - wandb integration
results = {
"ACC": acc,
"AP": ap,
- "R_ACC": r_acc,
+ "AUC": auc,
+ "R_ACC": r_acc,
"F_ACC": f_acc,
+ "TPR": tpr, # True Positive Rate / Recall
+ "TNR": tnr, # True Negative Rate / Specificity
}
return results
diff --git a/core/utils1/trainer.py b/core/utils1/trainer.py
index 8599603..99534d0 100644
--- a/core/utils1/trainer.py
+++ b/core/utils1/trainer.py
@@ -4,9 +4,9 @@
import torch.nn as nn
from torch.nn import init
-from utils1.config import CONFIGCLASS
-from utils1.utils import get_network
-from utils1.warmup import GradualWarmupScheduler
+from core.utils1.config import CONFIGCLASS
+from core.utils1.utils import get_network
+from core.utils1.warmup import GradualWarmupScheduler
class BaseModel(nn.Module):
@@ -18,9 +18,9 @@ def __init__(self, cfg: CONFIGCLASS):
self.save_dir = cfg.ckpt_dir
self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
self.model:nn.Module
- self.model=nn.Module.to(self.device)
# self.model.to(self.device)
#self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
+ # removed self.model=nnModule.to(self.device) breaks the model assignment
self.optimizer: torch.optim.Optimizer
def save_networks(self, epoch: int):
diff --git a/core/utils1/utils1/__pycache__/config.cpython-310.pyc b/core/utils1/utils1/__pycache__/config.cpython-310.pyc
deleted file mode 100644
index 4df9fd8..0000000
Binary files a/core/utils1/utils1/__pycache__/config.cpython-310.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/config.cpython-38.pyc b/core/utils1/utils1/__pycache__/config.cpython-38.pyc
deleted file mode 100644
index c1844e7..0000000
Binary files a/core/utils1/utils1/__pycache__/config.cpython-38.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/config.cpython-39.pyc b/core/utils1/utils1/__pycache__/config.cpython-39.pyc
deleted file mode 100644
index 449975b..0000000
Binary files a/core/utils1/utils1/__pycache__/config.cpython-39.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/datasets.cpython-310.pyc b/core/utils1/utils1/__pycache__/datasets.cpython-310.pyc
deleted file mode 100644
index 256dbf6..0000000
Binary files a/core/utils1/utils1/__pycache__/datasets.cpython-310.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/datasets.cpython-38.pyc b/core/utils1/utils1/__pycache__/datasets.cpython-38.pyc
deleted file mode 100644
index bd1437e..0000000
Binary files a/core/utils1/utils1/__pycache__/datasets.cpython-38.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/datasets.cpython-39.pyc b/core/utils1/utils1/__pycache__/datasets.cpython-39.pyc
deleted file mode 100644
index 47acb95..0000000
Binary files a/core/utils1/utils1/__pycache__/datasets.cpython-39.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/earlystop.cpython-310.pyc b/core/utils1/utils1/__pycache__/earlystop.cpython-310.pyc
deleted file mode 100644
index 67deb7f..0000000
Binary files a/core/utils1/utils1/__pycache__/earlystop.cpython-310.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/earlystop.cpython-38.pyc b/core/utils1/utils1/__pycache__/earlystop.cpython-38.pyc
deleted file mode 100644
index 071bef5..0000000
Binary files a/core/utils1/utils1/__pycache__/earlystop.cpython-38.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/eval.cpython-310.pyc b/core/utils1/utils1/__pycache__/eval.cpython-310.pyc
deleted file mode 100644
index ffcbe35..0000000
Binary files a/core/utils1/utils1/__pycache__/eval.cpython-310.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/eval.cpython-38.pyc b/core/utils1/utils1/__pycache__/eval.cpython-38.pyc
deleted file mode 100644
index 5fede42..0000000
Binary files a/core/utils1/utils1/__pycache__/eval.cpython-38.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/eval.cpython-39.pyc b/core/utils1/utils1/__pycache__/eval.cpython-39.pyc
deleted file mode 100644
index f028dc2..0000000
Binary files a/core/utils1/utils1/__pycache__/eval.cpython-39.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/trainer.cpython-310.pyc b/core/utils1/utils1/__pycache__/trainer.cpython-310.pyc
deleted file mode 100644
index 607a7f2..0000000
Binary files a/core/utils1/utils1/__pycache__/trainer.cpython-310.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/trainer.cpython-38.pyc b/core/utils1/utils1/__pycache__/trainer.cpython-38.pyc
deleted file mode 100644
index 015428d..0000000
Binary files a/core/utils1/utils1/__pycache__/trainer.cpython-38.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/utils.cpython-310.pyc b/core/utils1/utils1/__pycache__/utils.cpython-310.pyc
deleted file mode 100644
index e17d076..0000000
Binary files a/core/utils1/utils1/__pycache__/utils.cpython-310.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/utils.cpython-38.pyc b/core/utils1/utils1/__pycache__/utils.cpython-38.pyc
deleted file mode 100644
index 5bd02d9..0000000
Binary files a/core/utils1/utils1/__pycache__/utils.cpython-38.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/utils.cpython-39.pyc b/core/utils1/utils1/__pycache__/utils.cpython-39.pyc
deleted file mode 100644
index 489fc56..0000000
Binary files a/core/utils1/utils1/__pycache__/utils.cpython-39.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/warmup.cpython-310.pyc b/core/utils1/utils1/__pycache__/warmup.cpython-310.pyc
deleted file mode 100644
index ab707b7..0000000
Binary files a/core/utils1/utils1/__pycache__/warmup.cpython-310.pyc and /dev/null differ
diff --git a/core/utils1/utils1/__pycache__/warmup.cpython-38.pyc b/core/utils1/utils1/__pycache__/warmup.cpython-38.pyc
deleted file mode 100644
index c1da684..0000000
Binary files a/core/utils1/utils1/__pycache__/warmup.cpython-38.pyc and /dev/null differ
diff --git a/core/utils1/utils1/config.py b/core/utils1/utils1/config.py
deleted file mode 100644
index e16101d..0000000
--- a/core/utils1/utils1/config.py
+++ /dev/null
@@ -1,157 +0,0 @@
-import argparse
-import os
-import sys
-from abc import ABC
-from typing import Type
-
-
-class DefaultConfigs(ABC):
- ####### base setting ######
- gpus = [0]
- seed = 3407
- arch = "resnet50"
- datasets = ["zhaolian_train"]
- datasets_test = ["adm_res_abs_ddim20s"]
- mode = "binary"
- class_bal = False
- batch_size = 64
- loadSize = 256
- cropSize = 224
- epoch = "latest"
- num_workers = 20
- serial_batches = False
- isTrain = True
-
- # data augmentation
- rz_interp = ["bilinear"]
- # blur_prob = 0.0
- blur_prob = 0.1
- blur_sig = [0.5]
- # jpg_prob = 0.0
- jpg_prob = 0.1
- jpg_method = ["cv2"]
- jpg_qual = [75]
- gray_prob = 0.0
- aug_resize = True
- aug_crop = True
- aug_flip = True
- aug_norm = True
-
- ####### train setting ######
- warmup = False
- # warmup = True
- warmup_epoch = 3
- earlystop = True
- earlystop_epoch = 5
- optim = "adam"
- new_optim = False
- loss_freq = 400
- save_latest_freq = 2000
- save_epoch_freq = 20
- continue_train = False
- epoch_count = 1
- last_epoch = -1
- nepoch = 400
- beta1 = 0.9
- lr = 0.0001
- init_type = "normal"
- init_gain = 0.02
- pretrained = True
-
- # paths information
- root_dir1 = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
- root_dir = os.path.dirname(root_dir1)
- dataset_root = os.path.join(root_dir, "data")
- exp_root = os.path.join(root_dir, "data", "exp")
- _exp_name = ""
- exp_dir = ""
- ckpt_dir = ""
- logs_path = ""
- ckpt_path = ""
-
- @property
- def exp_name(self):
- return self._exp_name
-
- @exp_name.setter
- def exp_name(self, value: str):
- self._exp_name = value
- self.exp_dir: str = os.path.join(self.exp_root, self.exp_name)
- self.ckpt_dir: str = os.path.join(self.exp_dir, "ckpt")
- self.logs_path: str = os.path.join(self.exp_dir, "logs.txt")
-
- os.makedirs(self.exp_dir, exist_ok=True)
- os.makedirs(self.ckpt_dir, exist_ok=True)
-
- def to_dict(self):
- dic = {}
- for fieldkey in dir(self):
- fieldvalue = getattr(self, fieldkey)
- if not fieldkey.startswith("__") and not callable(fieldvalue) and not fieldkey.startswith("_"):
- dic[fieldkey] = fieldvalue
- return dic
-
-
-def args_list2dict(arg_list: list):
- assert len(arg_list) % 2 == 0, f"Override list has odd length: {arg_list}; it must be a list of pairs"
- return dict(zip(arg_list[::2], arg_list[1::2]))
-
-
-def str2bool(v: str) -> bool:
- if isinstance(v, bool):
- return v
- elif v.lower() in ("true", "yes", "on", "y", "t", "1"):
- return True
- elif v.lower() in ("false", "no", "off", "n", "f", "0"):
- return False
- else:
- return bool(v)
-
-
-def str2list(v: str, element_type=None) -> list:
- if not isinstance(v, (list, tuple, set)):
- v = v.lstrip("[").rstrip("]")
- v = v.split(",")
- v = list(map(str.strip, v))
- if element_type is not None:
- v = list(map(element_type, v))
- return v
-
-
-CONFIGCLASS = Type[DefaultConfigs]
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--gpus", default=[0], type=int, nargs="+")
-parser.add_argument("--exp_name", default="", type=str)
-parser.add_argument("--ckpt", default="model_epoch_latest.pth", type=str)
-parser.add_argument("opts", default=[], nargs=argparse.REMAINDER)
-args = parser.parse_args()
-
-if os.path.exists(os.path.join(DefaultConfigs.exp_root, args.exp_name, "config.py")):
- sys.path.insert(0, os.path.join(DefaultConfigs.exp_root, args.exp_name))
- from config import cfg
-
- cfg: CONFIGCLASS
-else:
- cfg = DefaultConfigs()
-
-if args.opts:
- opts = args_list2dict(args.opts)
- for k, v in opts.items():
- if not hasattr(cfg, k):
- raise ValueError(f"Unrecognized option: {k}")
- original_type = type(getattr(cfg, k))
- if original_type == bool:
- setattr(cfg, k, str2bool(v))
- elif original_type in (list, tuple, set):
- setattr(cfg, k, str2list(v, type(getattr(cfg, k)[0])))
- else:
- setattr(cfg, k, original_type(v))
-
-cfg.gpus: list = args.gpus
-os.environ["CUDA_VISIBLE_DEVICES"] = ", ".join([str(gpu) for gpu in cfg.gpus])
-cfg.exp_name = args.exp_name
-cfg.ckpt_path: str = os.path.join(cfg.ckpt_dir, args.ckpt)
-
-if isinstance(cfg.datasets, str):
- cfg.datasets = cfg.datasets.split(",")
diff --git a/core/utils1/utils1/datasets.py b/core/utils1/utils1/datasets.py
deleted file mode 100644
index a35863a..0000000
--- a/core/utils1/utils1/datasets.py
+++ /dev/null
@@ -1,178 +0,0 @@
-import os
-from io import BytesIO
-from random import choice, random
-
-import cv2
-import numpy as np
-import torch
-import torch.utils.data
-import torchvision.datasets as datasets
-import torchvision.transforms as transforms
-import torchvision.transforms.functional as TF
-from PIL import Image, ImageFile
-from scipy.ndimage import gaussian_filter
-from torch.utils.data.sampler import WeightedRandomSampler
-
-from .config import CONFIGCLASS
-
-ImageFile.LOAD_TRUNCATED_IMAGES = True
-
-
-def dataset_folder(root: str, cfg: CONFIGCLASS):
- if cfg.mode == "binary":
- return binary_dataset(root, cfg)
- if cfg.mode == "filename":
- return FileNameDataset(root, cfg)
- raise ValueError("cfg.mode needs to be binary or filename.")
-
-
-def binary_dataset(root: str, cfg: CONFIGCLASS):
- identity_transform = transforms.Lambda(lambda img: img)
-
- rz_func = identity_transform
-
- if cfg.isTrain:
- crop_func = transforms.RandomCrop((448,448))
- else:
- crop_func = transforms.CenterCrop((448,448)) if cfg.aug_crop else identity_transform
-
- if cfg.isTrain and cfg.aug_flip:
- flip_func = transforms.RandomHorizontalFlip()
- else:
- flip_func = identity_transform
-
-
- return datasets.ImageFolder(
- root,
- transforms.Compose(
- [
- rz_func,
- #change
- transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
- crop_func,
- flip_func,
- transforms.ToTensor(),
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
- if cfg.aug_norm
- else identity_transform,
- ]
- )
- )
-
-
-class FileNameDataset(datasets.ImageFolder):
- def name(self):
- return 'FileNameDataset'
-
- def __init__(self, opt, root):
- self.opt = opt
- super().__init__(root)
-
- def __getitem__(self, index):
- # Loading sample
- path, target = self.samples[index]
- return path
-
-
-def blur_jpg_augment(img: Image.Image, cfg: CONFIGCLASS):
- img: np.ndarray = np.array(img)
- if cfg.isTrain:
- if random() < cfg.blur_prob:
- sig = sample_continuous(cfg.blur_sig)
- gaussian_blur(img, sig)
-
- if random() < cfg.jpg_prob:
- method = sample_discrete(cfg.jpg_method)
- qual = sample_discrete(cfg.jpg_qual)
- img = jpeg_from_key(img, qual, method)
-
- return Image.fromarray(img)
-
-
-def sample_continuous(s: list):
- if len(s) == 1:
- return s[0]
- if len(s) == 2:
- rg = s[1] - s[0]
- return random() * rg + s[0]
- raise ValueError("Length of iterable s should be 1 or 2.")
-
-
-def sample_discrete(s: list):
- return s[0] if len(s) == 1 else choice(s)
-
-
-def gaussian_blur(img: np.ndarray, sigma: float):
- gaussian_filter(img[:, :, 0], output=img[:, :, 0], sigma=sigma)
- gaussian_filter(img[:, :, 1], output=img[:, :, 1], sigma=sigma)
- gaussian_filter(img[:, :, 2], output=img[:, :, 2], sigma=sigma)
-
-
-def cv2_jpg(img: np.ndarray, compress_val: int) -> np.ndarray:
- img_cv2 = img[:, :, ::-1]
- encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), compress_val]
- result, encimg = cv2.imencode(".jpg", img_cv2, encode_param)
- decimg = cv2.imdecode(encimg, 1)
- return decimg[:, :, ::-1]
-
-
-def pil_jpg(img: np.ndarray, compress_val: int):
- out = BytesIO()
- img = Image.fromarray(img)
- img.save(out, format="jpeg", quality=compress_val)
- img = Image.open(out)
- # load from memory before ByteIO closes
- img = np.array(img)
- out.close()
- return img
-
-
-jpeg_dict = {"cv2": cv2_jpg, "pil": pil_jpg}
-
-
-def jpeg_from_key(img: np.ndarray, compress_val: int, key: str) -> np.ndarray:
- method = jpeg_dict[key]
- return method(img, compress_val)
-
-
-rz_dict = {'bilinear': Image.BILINEAR,
- 'bicubic': Image.BICUBIC,
- 'lanczos': Image.LANCZOS,
- 'nearest': Image.NEAREST}
-def custom_resize(img: Image.Image, cfg: CONFIGCLASS) -> Image.Image:
- interp = sample_discrete(cfg.rz_interp)
- return TF.resize(img, cfg.loadSize, interpolation=rz_dict[interp])
-
-
-def get_dataset(cfg: CONFIGCLASS):
- dset_lst = []
- for dataset in cfg.datasets:
- root = os.path.join(cfg.dataset_root, dataset)
- dset = dataset_folder(root, cfg)
- dset_lst.append(dset)
- return torch.utils.data.ConcatDataset(dset_lst)
-
-
-def get_bal_sampler(dataset: torch.utils.data.ConcatDataset):
- targets = []
- for d in dataset.datasets:
- targets.extend(d.targets)
-
- ratio = np.bincount(targets)
- w = 1.0 / torch.tensor(ratio, dtype=torch.float)
- sample_weights = w[targets]
- return WeightedRandomSampler(weights=sample_weights, num_samples=len(sample_weights))
-
-
-def create_dataloader(cfg: CONFIGCLASS):
- shuffle = not cfg.serial_batches if (cfg.isTrain and not cfg.class_bal) else False
- dataset = get_dataset(cfg)
- sampler = get_bal_sampler(dataset) if cfg.class_bal else None
-
- return torch.utils.data.DataLoader(
- dataset,
- batch_size=cfg.batch_size,
- shuffle=shuffle,
- sampler=sampler,
- num_workers=int(cfg.num_workers),
- )
diff --git a/core/utils1/utils1/earlystop.py b/core/utils1/utils1/earlystop.py
deleted file mode 100644
index 25aef71..0000000
--- a/core/utils1/utils1/earlystop.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import numpy as np
-
-from .trainer import Trainer
-
-
-class EarlyStopping:
- """Early stops the training if validation loss doesn't improve after a given patience."""
-
- def __init__(self, patience=1, verbose=False, delta=0):
- """
- Args:
- patience (int): How long to wait after last time validation loss improved.
- Default: 7
- verbose (bool): If True, prints a message for each validation loss improvement.
- Default: False
- delta (float): Minimum change in the monitored quantity to qualify as an improvement.
- Default: 0
- """
- self.patience = patience
- self.verbose = verbose
- self.counter = 0
- self.best_score = None
- self.early_stop = False
- self.score_max = -np.Inf
- self.delta = delta
-
- def __call__(self, score: float, trainer: Trainer):
- if self.best_score is None:
- self.best_score = score
- self.save_checkpoint(score, trainer)
- elif score < self.best_score - self.delta:
- self.counter += 1
- print(f"EarlyStopping counter: {self.counter} out of {self.patience}")
- if self.counter >= self.patience:
- self.early_stop = True
- else:
- self.best_score = score
- self.save_checkpoint(score, trainer)
- self.counter = 0
-
- def save_checkpoint(self, score: float, trainer: Trainer):
- """Saves model when validation loss decrease."""
- if self.verbose:
- print(f"Validation accuracy increased ({self.score_max:.6f} --> {score:.6f}). Saving model ...")
- trainer.save_networks("best")
- self.score_max = score
diff --git a/core/utils1/utils1/eval.py b/core/utils1/utils1/eval.py
deleted file mode 100644
index 5784de3..0000000
--- a/core/utils1/utils1/eval.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import math
-import os
-
-import matplotlib.pyplot as plt
-import numpy as np
-import torch
-import torch.nn as nn
-
-from .config import CONFIGCLASS
-from .utils import to_cuda
-
-
-def get_val_cfg(cfg: CONFIGCLASS, split="val", copy=True):
- if copy:
- from copy import deepcopy
-
- val_cfg = deepcopy(cfg)
- else:
- val_cfg = cfg
- val_cfg.dataset_root = os.path.join(val_cfg.dataset_root, split)
- val_cfg.datasets = cfg.datasets_test
- val_cfg.isTrain = False
- # val_cfg.aug_resize = False
- # val_cfg.aug_crop = False
- val_cfg.aug_flip = False
- val_cfg.serial_batches = True
- val_cfg.jpg_method = ["pil"]
- # Currently assumes jpg_prob, blur_prob 0 or 1
- if len(val_cfg.blur_sig) == 2:
- b_sig = val_cfg.blur_sig
- val_cfg.blur_sig = [(b_sig[0] + b_sig[1]) / 2]
- if len(val_cfg.jpg_qual) != 1:
- j_qual = val_cfg.jpg_qual
- val_cfg.jpg_qual = [int((j_qual[0] + j_qual[-1]) / 2)]
- return val_cfg
-
-def validate(model: nn.Module, cfg: CONFIGCLASS):
- from sklearn.metrics import accuracy_score, average_precision_score, roc_auc_score
-
- from .datasets import create_dataloader
-
- data_loader = create_dataloader(cfg)
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
-
- with torch.no_grad():
- y_true, y_pred = [], []
- for data in data_loader:
- img, label, meta = data if len(data) == 3 else (*data, None)
- in_tens = to_cuda(img, device)
- meta = to_cuda(meta, device)
- predict = model(in_tens, meta).sigmoid()
- y_pred.extend(predict.flatten().tolist())
- y_true.extend(label.flatten().tolist())
-
- y_true, y_pred = np.array(y_true), np.array(y_pred)
- r_acc = accuracy_score(y_true[y_true == 0], y_pred[y_true == 0] > 0.5)
- f_acc = accuracy_score(y_true[y_true == 1], y_pred[y_true == 1] > 0.5)
- acc = accuracy_score(y_true, y_pred > 0.5)
- ap = average_precision_score(y_true, y_pred)
- results = {
- "ACC": acc,
- "AP": ap,
- "R_ACC": r_acc,
- "F_ACC": f_acc,
- }
- return results
diff --git a/core/utils1/utils1/trainer.py b/core/utils1/utils1/trainer.py
deleted file mode 100644
index 7a4ce8b..0000000
--- a/core/utils1/utils1/trainer.py
+++ /dev/null
@@ -1,169 +0,0 @@
-import os
-
-import torch
-import torch.nn as nn
-from torch.nn import init
-
-from .config import CONFIGCLASS
-from .utils import get_network
-from .warmup import GradualWarmupScheduler
-
-
-class BaseModel(nn.Module):
- def __init__(self, cfg: CONFIGCLASS):
- super().__init__()
- self.cfg = cfg
- self.total_steps = 0
- self.isTrain = cfg.isTrain
- self.save_dir = cfg.ckpt_dir
- self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
- self.model:nn.Module
- self.model=nn.Module.to(self.device)
- # self.model.to(self.device)
- self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
- self.optimizer: torch.optim.Optimizer
-
- def save_networks(self, epoch: int):
- save_filename = f"model_epoch_{epoch}.pth"
- save_path = os.path.join(self.save_dir, save_filename)
-
- # serialize model and optimizer to dict
- state_dict = {
- "model": self.model.state_dict(),
- "optimizer": self.optimizer.state_dict(),
- "total_steps": self.total_steps,
- }
-
- torch.save(state_dict, save_path)
-
- # load models from the disk
- def load_networks(self, epoch: int):
- load_filename = f"model_epoch_{epoch}.pth"
- load_path = os.path.join(self.save_dir, load_filename)
-
- if epoch==0:
- # load_filename = f"lsun_adm.pth"
- load_path="checkpoints/optical.pth"
- print("loading optical path")
- else :
- print(f"loading the model from {load_path}")
-
- # print(f"loading the model from {load_path}")
-
- # if you are using PyTorch newer than 0.4 (e.g., built from
- # GitHub source), you can remove str() on self.device
- state_dict = torch.load(load_path, map_location=self.device)
- if hasattr(state_dict, "_metadata"):
- del state_dict._metadata
-
- self.model.load_state_dict(state_dict["model"])
- self.total_steps = state_dict["total_steps"]
-
- if self.isTrain and not self.cfg.new_optim:
- self.optimizer.load_state_dict(state_dict["optimizer"])
- # move optimizer state to GPU
- for state in self.optimizer.state.values():
- for k, v in state.items():
- if torch.is_tensor(v):
- state[k] = v.to(self.device)
-
- for g in self.optimizer.param_groups:
- g["lr"] = self.cfg.lr
-
- def eval(self):
- self.model.eval()
-
- def test(self):
- with torch.no_grad():
- self.forward()
-
-
-def init_weights(net: nn.Module, init_type="normal", gain=0.02):
- def init_func(m: nn.Module):
- classname = m.__class__.__name__
- if hasattr(m, "weight") and (classname.find("Conv") != -1 or classname.find("Linear") != -1):
- if init_type == "normal":
- init.normal_(m.weight.data, 0.0, gain)
- elif init_type == "xavier":
- init.xavier_normal_(m.weight.data, gain=gain)
- elif init_type == "kaiming":
- init.kaiming_normal_(m.weight.data, a=0, mode="fan_in")
- elif init_type == "orthogonal":
- init.orthogonal_(m.weight.data, gain=gain)
- else:
- raise NotImplementedError(f"initialization method [{init_type}] is not implemented")
- if hasattr(m, "bias") and m.bias is not None:
- init.constant_(m.bias.data, 0.0)
- elif classname.find("BatchNorm2d") != -1:
- init.normal_(m.weight.data, 1.0, gain)
- init.constant_(m.bias.data, 0.0)
-
- print(f"initialize network with {init_type}")
- net.apply(init_func)
-
-
-class Trainer(BaseModel):
- def name(self):
- return "Trainer"
-
- def __init__(self, cfg: CONFIGCLASS):
- super().__init__(cfg)
- self.arch = cfg.arch
- self.model = get_network(self.arch, cfg.isTrain, cfg.continue_train, cfg.init_gain, cfg.pretrained)
-
- self.loss_fn = nn.BCEWithLogitsLoss()
- # initialize optimizers
- if cfg.optim == "adam":
- self.optimizer = torch.optim.Adam(self.model.parameters(), lr=cfg.lr, betas=(cfg.beta1, 0.999))
- elif cfg.optim == "sgd":
- self.optimizer = torch.optim.SGD(self.model.parameters(), lr=cfg.lr, momentum=0.9, weight_decay=5e-4)
- else:
- raise ValueError("optim should be [adam, sgd]")
- if cfg.warmup:
- scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR(
- self.optimizer, cfg.nepoch - cfg.warmup_epoch, eta_min=1e-6
- )
- self.scheduler = GradualWarmupScheduler(
- self.optimizer, multiplier=1, total_epoch=cfg.warmup_epoch, after_scheduler=scheduler_cosine
- )
- self.scheduler.step()
- if cfg.continue_train:
- self.load_networks(cfg.epoch)
- self.model.to(self.device)
-
- # self.model.load_state_dict(torch.load('checkpoints/optical.pth'))
- load_path='checkpoints/optical.pth'
- state_dict = torch.load(load_path, map_location=self.device)
-
-
- self.model.load_state_dict(state_dict["model"])
-
-
- def adjust_learning_rate(self, min_lr=1e-6):
- for param_group in self.optimizer.param_groups:
- param_group["lr"] /= 10.0
- if param_group["lr"] < min_lr:
- return False
- return True
-
- def set_input(self, input):
- img, label, meta = input if len(input) == 3 else (input[0], input[1], {})
- self.input = img.to(self.device)
- self.label = label.to(self.device).float()
- for k in meta.keys():
- if isinstance(meta[k], torch.Tensor):
- meta[k] = meta[k].to(self.device)
- self.meta = meta
-
- def forward(self):
- self.output = self.model(self.input, self.meta)
-
- def get_loss(self):
- return self.loss_fn(self.output.squeeze(1), self.label)
-
- def optimize_parameters(self):
- self.forward()
- self.loss = self.loss_fn(self.output.squeeze(1), self.label)
- self.optimizer.zero_grad()
- self.loss.backward()
- self.optimizer.step()
diff --git a/core/utils1/utils1/utils.py b/core/utils1/utils1/utils.py
deleted file mode 100644
index d52ebbd..0000000
--- a/core/utils1/utils1/utils.py
+++ /dev/null
@@ -1,109 +0,0 @@
-import argparse
-import os
-import sys
-import time
-import warnings
-from importlib import import_module
-
-import numpy as np
-import torch
-import torch.nn as nn
-from PIL import Image
-
-warnings.filterwarnings("ignore", category=UserWarning, module="torch.nn.functional")
-
-
-def str2bool(v: str, strict=True) -> bool:
- if isinstance(v, bool):
- return v
- elif isinstance(v, str):
- if v.lower() in ("true", "yes", "on" "t", "y", "1"):
- return True
- elif v.lower() in ("false", "no", "off", "f", "n", "0"):
- return False
- if strict:
- raise argparse.ArgumentTypeError("Unsupported value encountered.")
- else:
- return True
-
-
-def to_cuda(data, device="cuda", exclude_keys: "list[str]" = None):
- if isinstance(data, torch.Tensor):
- data = data.to(device)
- elif isinstance(data, (tuple, list, set)):
- data = [to_cuda(b, device) for b in data]
- elif isinstance(data, dict):
- if exclude_keys is None:
- exclude_keys = []
- for k in data.keys():
- if k not in exclude_keys:
- data[k] = to_cuda(data[k], device)
- else:
- # raise TypeError(f"Unsupported type: {type(data)}")
- data = data
- return data
-
-
-class HiddenPrints:
- def __enter__(self):
- self._original_stdout = sys.stdout
- sys.stdout = open(os.devnull, "w")
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- sys.stdout.close()
- sys.stdout = self._original_stdout
-
-
-class Logger(object):
- def __init__(self):
- self.terminal = sys.stdout
- self.file = None
-
- def open(self, file, mode=None):
- if mode is None:
- mode = "w"
- self.file = open(file, mode)
-
- def write(self, message, is_terminal=1, is_file=1):
- if "\r" in message:
- is_file = 0
- if is_terminal == 1:
- self.terminal.write(message)
- self.terminal.flush()
- if is_file == 1:
- self.file.write(message)
- self.file.flush()
-
- def flush(self):
- # this flush method is needed for python 3 compatibility.
- # this handles the flush command by doing nothing.
- # you might want to specify some extra behavior here.
- pass
-
-
-def get_network(arch: str, isTrain=False, continue_train=False, init_gain=0.02, pretrained=True):
- if "resnet" in arch:
- from networks.resnet import ResNet
-
- resnet = getattr(import_module("networks.resnet"), arch)
- if isTrain:
- if continue_train:
- model: ResNet = resnet(num_classes=1)
- else:
- model: ResNet = resnet(pretrained=pretrained)
- model.fc = nn.Linear(2048, 1)
- nn.init.normal_(model.fc.weight.data, 0.0, init_gain)
- else:
- model: ResNet = resnet(num_classes=1)
- return model
- else:
- raise ValueError(f"Unsupported arch: {arch}")
-
-
-def pad_img_to_square(img: np.ndarray):
- H, W = img.shape[:2]
- if H != W:
- new_size = max(H, W)
- img = np.pad(img, ((0, new_size - H), (0, new_size - W), (0, 0)), mode="constant")
- assert img.shape[0] == img.shape[1] == new_size
- return img
diff --git a/core/utils1/utils1/warmup.py b/core/utils1/utils1/warmup.py
deleted file mode 100644
index c193a6c..0000000
--- a/core/utils1/utils1/warmup.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from torch.optim.lr_scheduler import ReduceLROnPlateau, _LRScheduler
-
-
-class GradualWarmupScheduler(_LRScheduler):
- """Gradually warm-up(increasing) learning rate in optimizer.
- Proposed in 'Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour'.
-
- Args:
- optimizer (Optimizer): Wrapped optimizer.
- multiplier: target learning rate = base lr * multiplier if multiplier > 1.0. if multiplier = 1.0, lr starts from 0 and ends up with the base_lr.
- total_epoch: target learning rate is reached at total_epoch, gradually
- after_scheduler: after target_epoch, use this scheduler(eg. ReduceLROnPlateau)
- """
-
- def __init__(self, optimizer, multiplier, total_epoch, after_scheduler=None):
- self.multiplier = multiplier
- if self.multiplier < 1.0:
- raise ValueError("multiplier should be greater thant or equal to 1.")
- self.total_epoch = total_epoch
- self.after_scheduler = after_scheduler
- self.finished = False
- super().__init__(optimizer)
-
- def get_lr(self):
- if self.last_epoch > self.total_epoch:
- if self.after_scheduler:
- if not self.finished:
- self.after_scheduler.base_lrs = [base_lr * self.multiplier for base_lr in self.base_lrs]
- self.finished = True
- return self.after_scheduler.get_last_lr()
- return [base_lr * self.multiplier for base_lr in self.base_lrs]
-
- if self.multiplier == 1.0:
- return [base_lr * (float(self.last_epoch) / self.total_epoch) for base_lr in self.base_lrs]
- else:
- return [
- base_lr * ((self.multiplier - 1.0) * self.last_epoch / self.total_epoch + 1.0)
- for base_lr in self.base_lrs
- ]
-
- def step_ReduceLROnPlateau(self, metrics, epoch=None):
- if epoch is None:
- epoch = self.last_epoch + 1
- self.last_epoch = (
- epoch if epoch != 0 else 1
- ) # ReduceLROnPlateau is called at the end of epoch, whereas others are called at beginning
- if self.last_epoch <= self.total_epoch:
- warmup_lr = [
- base_lr * ((self.multiplier - 1.0) * self.last_epoch / self.total_epoch + 1.0)
- for base_lr in self.base_lrs
- ]
- for param_group, lr in zip(self.optimizer.param_groups, warmup_lr):
- param_group["lr"] = lr
- else:
- if epoch is None:
- self.after_scheduler.step(metrics, None)
- else:
- self.after_scheduler.step(metrics, epoch - self.total_epoch)
-
- def step(self, epoch=None, metrics=None):
- if type(self.after_scheduler) != ReduceLROnPlateau:
- if self.finished and self.after_scheduler:
- if epoch is None:
- self.after_scheduler.step(None)
- else:
- self.after_scheduler.step(epoch - self.total_epoch)
- else:
- return super().step(epoch)
- else:
- self.step_ReduceLROnPlateau(metrics, epoch)
diff --git a/demo.py b/demo.py
index 9700ef8..fc526e5 100644
--- a/demo.py
+++ b/demo.py
@@ -18,7 +18,7 @@
from utils import flow_viz
from utils.utils import InputPadder
from natsort import natsorted
-from utils1.utils import get_network, str2bool, to_cuda
+from core.utils1.utils import get_network, str2bool, to_cuda
from sklearn.metrics import accuracy_score, average_precision_score, roc_auc_score,roc_auc_score
@@ -44,9 +44,9 @@ def viz(img, flo, folder_optical_flow_path, imfile1):
# print(folder_optical_flow_path)
- parts=imfile1.rsplit('\\',1)
- content=parts[1]
- folder_optical_flow_path=folder_optical_flow_path+'/'+content.strip()
+ # Use os.path.basename to get filename (works on both Windows and Linux)
+ content = os.path.basename(imfile1)
+ folder_optical_flow_path = os.path.join(folder_optical_flow_path, content)
print(folder_optical_flow_path)
cv2.imwrite(folder_optical_flow_path, flo)
@@ -55,19 +55,24 @@ def video_to_frames(video_path, output_folder):
if not os.path.exists(output_folder):
os.makedirs(output_folder)
+ print(f"Extracting frames from video: {video_path}")
cap = cv2.VideoCapture(video_path)
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_count = 0
- while cap.isOpened():
- ret, frame = cap.read()
- if not ret:
- break
-
- frame_filename = os.path.join(output_folder, f"frame_{frame_count:05d}.png")
- cv2.imwrite(frame_filename, frame)
- frame_count += 1
+ with tqdm(total=total_frames, desc="Extracting frames", unit="frame") as pbar:
+ while cap.isOpened():
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ frame_filename = os.path.join(output_folder, f"frame_{frame_count:05d}.png")
+ cv2.imwrite(frame_filename, frame)
+ frame_count += 1
+ pbar.update(1)
cap.release()
+ print(f"✓ Extracted {frame_count} frames")
images = glob.glob(os.path.join(output_folder, '*.png')) + \
glob.glob(os.path.join(output_folder, '*.jpg'))
@@ -78,12 +83,14 @@ def video_to_frames(video_path, output_folder):
# generate optical flow images
def OF_gen(args):
+ print("Loading RAFT model...")
model = torch.nn.DataParallel(RAFT(args))
model.load_state_dict(torch.load(args.model, map_location=torch.device(DEVICE)))
model = model.module
model.to(DEVICE)
model.eval()
+ print("✓ RAFT model loaded")
if not os.path.exists(args.folder_optical_flow_path):
os.makedirs(args.folder_optical_flow_path)
@@ -94,7 +101,8 @@ def OF_gen(args):
images = video_to_frames(args.path, args.folder_original_path)
images = natsorted(images)
- for imfile1, imfile2 in zip(images[:-1], images[1:]):
+ print("Generating optical flow...")
+ for imfile1, imfile2 in tqdm(zip(images[:-1], images[1:]), total=len(images)-1, desc="Processing optical flow", unit="frame"):
image1 = load_image(imfile1)
image2 = load_image(imfile2)
@@ -104,6 +112,8 @@ def OF_gen(args):
flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
viz(image1, flow_up,args.folder_optical_flow_path,imfile1)
+
+ print("✓ Optical flow generation complete")
if __name__ == '__main__':
@@ -138,8 +148,16 @@ def OF_gen(args):
parser.add_argument("--aug_norm", type=str2bool, default=True)
args = parser.parse_args()
+ print("=" * 60)
+ print("AIGVDet - AI-Generated Video Detection")
+ print("=" * 60)
+ print(f"Input video: {args.path}")
+ print(f"Using device: {'GPU' if not args.use_cpu else 'CPU'}")
+ print("=" * 60)
+
OF_gen(args)
+ print("\nLoading detection models...")
model_op = get_network(args.arch)
state_dict = torch.load(args.model_optical_flow_path, map_location="cpu")
if "model" in state_dict:
@@ -148,6 +166,7 @@ def OF_gen(args):
model_op.eval()
if not args.use_cpu:
model_op.cuda()
+ print("✓ Optical flow model loaded")
model_or = get_network(args.arch)
state_dict = torch.load(args.model_original_path, map_location="cpu")
@@ -157,6 +176,7 @@ def OF_gen(args):
model_or.eval()
if not args.use_cpu:
model_or.cuda()
+ print("✓ RGB frame model loaded")
trans = transforms.Compose(
@@ -166,7 +186,9 @@ def OF_gen(args):
)
)
- print("*" * 30)
+ print("\n" + "=" * 60)
+ print("Running detection...")
+ print("=" * 60)
# optical_subfolder_path = args.folder_optical_flow_path
# original_subfolder_path = args.folder_original_path
@@ -176,9 +198,10 @@ def OF_gen(args):
optical_subsubfolder_path = args.folder_optical_flow_path
#RGB frame detection
+ print("\nAnalyzing RGB frames...")
original_file_list = sorted(glob.glob(os.path.join(original_subsubfolder_path, "*.jpg")) + glob.glob(os.path.join(original_subsubfolder_path, "*.png"))+glob.glob(os.path.join(original_subsubfolder_path, "*.JPEG")))
original_prob_sum=0
- for img_path in tqdm(original_file_list, dynamic_ncols=True, disable=len(original_file_list) <= 1):
+ for img_path in tqdm(original_file_list, desc="RGB detection", unit="frame"):
img = Image.open(img_path).convert("RGB")
img = trans(img)
@@ -196,12 +219,13 @@ def OF_gen(args):
original_predict=original_prob_sum/len(original_file_list)
- print("original prob",original_predict)
+ print(f"✓ RGB detection probability: {original_predict:.4f}")
#optical flow detection
+ print("\nAnalyzing optical flow...")
optical_file_list = sorted(glob.glob(os.path.join(optical_subsubfolder_path, "*.jpg")) + glob.glob(os.path.join(optical_subsubfolder_path, "*.png"))+glob.glob(os.path.join(optical_subsubfolder_path, "*.JPEG")))
optical_prob_sum=0
- for img_path in tqdm(optical_file_list, dynamic_ncols=True, disable=len(original_file_list) <= 1):
+ for img_path in tqdm(optical_file_list, desc="Optical flow detection", unit="frame"):
img = Image.open(img_path).convert("RGB")
img = trans(img)
@@ -217,11 +241,16 @@ def OF_gen(args):
optical_predict=optical_prob_sum/len(optical_file_list)
- print("optical prob",optical_predict)
+ print(f"✓ Optical flow detection probability: {optical_predict:.4f}")
predict=original_predict*0.5+optical_predict*0.5
- print(f"predict:{predict}")
+ print("\n" + "=" * 60)
+ print("RESULTS")
+ print("=" * 60)
+ print(f"Combined probability: {predict:.4f}")
+ print(f"Threshold: {args.threshold}")
if predict 500:
+ st.error(f"File size ({file_size_mb:.1f} MB) exceeds 500MB limit. Please upload a smaller model.")
+ raft_model_file = None
+ else:
+ st.success(f"Model size: {file_size_mb:.1f} MB")
+
+ # Video Upload (Batch or Solo)
+ uploaded_videos = st.file_uploader("Upload Video(s)", type=['mp4', 'avi', 'mov', 'mkv'], accept_multiple_files=True, key="video_uploader")
+
+ # Output Directory
+ output_root = st.text_input("Output Directory Root", value="output_data")
+
+ if st.button("Start Extraction", key="extract_btn"):
+ extraction_start_time = time.time()
+
+ if not raft_model_file:
+ st.error("Please upload the RAFT model.")
+ elif not uploaded_videos:
+ st.error("Please upload at least one video.")
+ else:
+ # Save RAFT model to temp file
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pth") as tmp_raft:
+ tmp_raft.write(raft_model_file.read())
+ raft_model_path = tmp_raft.name
+
+ st.info(f"Loaded RAFT model. Using device: {DEVICE}")
+
+ # Load RAFT
+ try:
+ # Args object for RAFT - needs to support 'in' operator
+ class Args:
+ def __init__(self):
+ self.model = raft_model_path
+ self.small = False
+ self.mixed_precision = False
+ self.alternate_corr = False
+
+ def __contains__(self, key):
+ return hasattr(self, key)
+
+ args = Args()
+ model = torch.nn.DataParallel(RAFT(args))
+ model.load_state_dict(torch.load(args.model, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+
+ st.success("RAFT Model Loaded Successfully!")
+
+ # Process Videos
+ total_videos = len(uploaded_videos)
+
+ for i, video_file in enumerate(uploaded_videos):
+ video_start_time = time.time()
+ video_name = video_file.name
+ st.subheader(f"Processing: {video_name} ({i+1}/{total_videos})")
+
+ # Save video to temp
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(video_name)[1]) as tmp_vid:
+ tmp_vid.write(video_file.read())
+ video_path = tmp_vid.name
+
+ # Define output paths
+ base_name = os.path.splitext(video_name)[0]
+ frame_output_dir = os.path.join(output_root, "frames", base_name)
+ flow_output_dir = os.path.join(output_root, "optical_flow", base_name)
+
+ # 1. Extract Frames
+ st.write("Extracting frames...")
+ frame_start = time.time()
+ p_bar = st.progress(0)
+ images = video_to_frames(video_path, frame_output_dir, p_bar)
+ frame_duration = time.time() - frame_start
+ st.write(f"✓ Extracted {len(images)} frames to `{frame_output_dir}` in {frame_duration:.2f}s")
+
+ # 2. Generate Optical Flow
+ if not os.path.exists(flow_output_dir):
+ os.makedirs(flow_output_dir)
+
+ st.write("Generating Optical Flow...")
+ flow_start = time.time()
+ images = natsorted(images)
+ flow_p_bar = st.progress(0)
+
+ with torch.no_grad():
+ for idx, (imfile1, imfile2) in enumerate(zip(images[:-1], images[1:])):
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+
+ save_vis(image1, flow_up, flow_output_dir, imfile1)
+
+ flow_p_bar.progress((idx + 1) / (len(images) - 1))
+
+ flow_duration = time.time() - flow_start
+ st.write(f"✓ Optical Flow saved to `{flow_output_dir}` in {flow_duration:.2f}s")
+
+ video_duration = time.time() - video_start_time
+ st.info(f"**Video processed in {video_duration:.2f} seconds**")
+
+ # Cleanup temp video
+ os.remove(video_path)
+
+ total_extraction_time = time.time() - extraction_start_time
+ st.success(f"✓ All videos processed! Total time: {total_extraction_time:.2f} seconds")
+ # Cleanup temp model
+ os.remove(raft_model_path)
+
+ except Exception as e:
+ st.error(f"An error occurred: {e}")
+ import traceback
+ st.code(traceback.format_exc())
+
+# --- TAB 2: DETECTION ---
+with tab2:
+ st.header("Run Detection (AIGVDet)")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ optical_model_file = st.file_uploader("Upload Optical Flow Model (optical.pth)", type=['pth'], key="opt_uploader")
+ if optical_model_file:
+ opt_size_mb = optical_model_file.size / (1024 * 1024)
+ if opt_size_mb > 500:
+ st.error(f"File size ({opt_size_mb:.1f} MB) exceeds 500MB limit.")
+ optical_model_file = None
+ else:
+ st.success(f"Optical model: {opt_size_mb:.1f} MB")
+
+ with col2:
+ original_model_file = st.file_uploader("Upload RGB Model (original.pth)", type=['pth'], key="orig_uploader")
+ if original_model_file:
+ orig_size_mb = original_model_file.size / (1024 * 1024)
+ if orig_size_mb > 500:
+ st.error(f"File size ({orig_size_mb:.1f} MB) exceeds 500MB limit.")
+ original_model_file = None
+ else:
+ st.success(f"RGB model: {orig_size_mb:.1f} MB")
+
+ # Input for processed data path
+ # Default to the output of Tab 1 if available
+ target_dir = st.text_input("Path to Processed Data (Root folder containing 'frames' and 'optical_flow')", value="output_data")
+
+ threshold = st.slider("Threshold", 0.0, 1.0, 0.5)
+
+ if st.button("Run Detection", key="detect_btn"):
+ if not optical_model_file or not original_model_file:
+ st.error("Please upload both Optical Flow and RGB models.")
+ elif not os.path.exists(target_dir):
+ st.error(f"Directory `{target_dir}` does not exist.")
+ else:
+ start_time = time.time()
+
+ # Save models to temp
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pth") as tmp_opt:
+ tmp_opt.write(optical_model_file.read())
+ opt_model_path = tmp_opt.name
+
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pth") as tmp_orig:
+ tmp_orig.write(original_model_file.read())
+ orig_model_path = tmp_orig.name
+
+ try:
+ st.info("Loading models...")
+
+ # Load Models
+ # Assuming ResNet50 as per demo.py default
+ model_op = get_network("resnet50")
+ state_dict_op = torch.load(opt_model_path, map_location="cpu")
+ if "model" in state_dict_op:
+ state_dict_op = state_dict_op["model"]
+ model_op.load_state_dict(state_dict_op)
+ model_op.eval()
+ if DEVICE == 'cuda':
+ model_op.cuda()
+
+ model_or = get_network("resnet50")
+ state_dict_or = torch.load(orig_model_path, map_location="cpu")
+ if "model" in state_dict_or:
+ state_dict_or = state_dict_or["model"]
+ model_or.load_state_dict(state_dict_or)
+ model_or.eval()
+ if DEVICE == 'cuda':
+ model_or.cuda()
+
+ load_duration = time.time() - start_time
+ st.write(f"Models loaded in {load_duration:.2f} seconds.")
+
+ # Find subfolders in frames
+ frames_root = os.path.join(target_dir, "frames")
+ flow_root = os.path.join(target_dir, "optical_flow")
+
+ if not os.path.exists(frames_root):
+ st.error(f"Could not find `frames` folder in {target_dir}")
+ st.stop()
+
+ # Get list of video folders
+ video_folders = [f for f in os.listdir(frames_root) if os.path.isdir(os.path.join(frames_root, f))]
+
+ if not video_folders:
+ st.warning("No subfolders found in `frames` directory.")
+
+ # Transforms
+ trans = transforms.Compose((
+ transforms.CenterCrop((448,448)),
+ transforms.ToTensor(),
+ ))
+
+ results = []
+
+ for vid_folder in video_folders:
+ video_detect_start = time.time()
+ st.subheader(f"Analyzing: {vid_folder}")
+
+ rgb_path = os.path.join(frames_root, vid_folder)
+ opt_path = os.path.join(flow_root, vid_folder)
+
+ if not os.path.exists(opt_path):
+ st.warning(f"No optical flow found for {vid_folder}, skipping.")
+ continue
+
+ # RGB Detection
+ rgb_files = sorted(glob.glob(os.path.join(rgb_path, "*.jpg")) +
+ glob.glob(os.path.join(rgb_path, "*.png")) +
+ glob.glob(os.path.join(rgb_path, "*.JPEG")))
+
+ rgb_prob_sum = 0
+ rgb_bar = st.progress(0, text="RGB Detection")
+ rgb_start = time.time()
+
+ for i, img_path in enumerate(rgb_files):
+ img = Image.open(img_path).convert("RGB")
+ img = trans(img)
+ img = TF.normalize(img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ in_tens = img.unsqueeze(0).to(DEVICE)
+
+ with torch.no_grad():
+ prob = model_or(in_tens).sigmoid().item()
+ rgb_prob_sum += prob
+
+ rgb_bar.progress((i + 1) / len(rgb_files))
+
+ rgb_duration = time.time() - rgb_start
+ rgb_score = rgb_prob_sum / len(rgb_files) if rgb_files else 0
+ st.write(f"✓ RGB Score: {rgb_score:.4f} ({len(rgb_files)} frames in {rgb_duration:.2f}s)")
+
+ # Optical Flow Detection
+ opt_files = sorted(glob.glob(os.path.join(opt_path, "*.jpg")) +
+ glob.glob(os.path.join(opt_path, "*.png")) +
+ glob.glob(os.path.join(opt_path, "*.JPEG")))
+
+ opt_prob_sum = 0
+ opt_bar = st.progress(0, text="Optical Flow Detection")
+ opt_start = time.time()
+
+ for i, img_path in enumerate(opt_files):
+ img = Image.open(img_path).convert("RGB")
+ img = trans(img)
+ img = TF.normalize(img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ in_tens = img.unsqueeze(0).to(DEVICE)
+
+ with torch.no_grad():
+ prob = model_op(in_tens).sigmoid().item()
+ opt_prob_sum += prob
+
+ opt_bar.progress((i + 1) / len(opt_files))
+
+ opt_duration = time.time() - opt_start
+ opt_score = opt_prob_sum / len(opt_files) if opt_files else 0
+ st.write(f"✓ Optical Flow Score: {opt_score:.4f} ({len(opt_files)} frames in {opt_duration:.2f}s)")
+
+ # Final Decision
+ final_score = (rgb_score * 0.5) + (opt_score * 0.5)
+ decision = "FAKE VIDEO (AI-Generated)" if final_score >= threshold else "REAL VIDEO"
+ color = "red" if final_score >= threshold else "green"
+
+ video_detect_duration = time.time() - video_detect_start
+
+ st.markdown(f"### Result: :{color}[{decision}]")
+ st.write(f"**Combined Probability:** {final_score:.4f}")
+ st.write(f"**Detection Time:** {video_detect_duration:.2f}s")
+ st.divider()
+
+ results.append({
+ "Video": vid_folder,
+ "RGB Score": rgb_score,
+ "Optical Score": opt_score,
+ "Final Score": final_score,
+ "Decision": decision
+ })
+
+ # Summary Table
+ if results:
+ st.subheader("Batch Summary")
+ st.dataframe(results)
+
+ total_duration = time.time() - start_time
+ st.success(f"Total Processing Time: {total_duration:.2f} seconds")
+
+ # Cleanup
+ os.remove(opt_model_path)
+ os.remove(orig_model_path)
+
+ except Exception as e:
+ st.error(f"An error occurred during detection: {e}")
+ import traceback
+ st.code(traceback.format_exc())
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..b3766a6
--- /dev/null
+++ b/main.py
@@ -0,0 +1,6 @@
+def main():
+ print("Hello from aigvdet!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/md-files/DATA_SETUP.md b/md-files/DATA_SETUP.md
new file mode 100644
index 0000000..ddcb814
--- /dev/null
+++ b/md-files/DATA_SETUP.md
@@ -0,0 +1,420 @@
+# Data Setup Instructions
+
+## Required Datasets
+
+### Training Data
+- Download from Baiduyun Link (extract code: ra95)
+- Extract to: `./data/train/`
+
+### Test Data
+**Note:** Training/validation data are frame sequences (PNG), but testing typically needs video files.
+
+**Solutions:**
+1. **Reconstruct videos from frames:** Convert frame sequences back to videos for testing
+2. **Use alternative video datasets:**
+ - FaceForensics++: https://github.com/ondyari/FaceForensics (has videos)
+ - Celeb-DF: https://github.com/yuezunli/celeb-deepfakeforensics
+ - DFDC: https://dfdc.ai/
+3. **Test with frame sequences:** Modify test scripts to work with PNG sequences
+4. **Create test split:** Use some frame sequences as test data
+
+### Data Structure
+```
+data/
+├── train/
+│ └── trainset_1/
+│ ├── 0_real/
+│ │ ├── video_00000/
+│ │ │ ├── 00000.png
+│ │ │ └── ...
+│ │ └── ...
+│ └── 1_fake/
+│ └── ...
+├── val/
+│ └── val_set_1/
+│ └── ... (same structure)
+└── test/
+ └── testset_1/
+ └── ... (same structure)
+```
+
+## Getting the Original Data
+
+### Step 1: Download Training Data
+**Original Baiduyun Link:** https://pan.baidu.com/s/17xmDyFjtcmNsoxmUeImMTQ?pwd=ra95
+- Extract code: `ra95`
+- Download the preprocessed training frames
+- Extract to: `./data/train/`
+
+### Step 2: Download Test Videos
+**Original Google Drive Link:** https://drive.google.com/drive/folders/1D84SRWEJ8BK8KBpTMuGi3BUM80mW_dKb?usp=sharing
+- Download test videos
+- Extract to: `./data/test/`
+
+### Step 3: Download Model Weights
+**Model Checkpoints:** https://drive.google.com/drive/folders/18JO_YxOEqwJYfbVvy308XjoV-N6fE4yP?usp=share_link
+- Download trained model weights
+- Move to: `./checkpoints/`
+
+**RAFT Model (for demo.py):** https://drive.google.com/file/d/1MqDajR89k-xLV0HIrmJ0k-n8ZpG6_suM/view
+- Download RAFT model weights
+- Move to: `./raft_model/`
+
+### Backup Plan: If Original Links Are Broken
+1. Contact paper authors: lyan924@cuc.edu.cn
+2. Check paper's GitHub issues for updated links
+3. Look for replication studies with alternative datasets
+
+### Step 3: Alternative if Original Data Unavailable
+- Use FaceForensics++ dataset (widely used benchmark)
+- Download from: https://github.com/ondyari/FaceForensics
+- Convert to the same folder structure
+
+## Docker Usage
+```bash
+# Windows
+docker run -it -v C:\path\to\your\data:/app/data sacdalance/thesis-aigvdet:gpu bash
+
+# Linux/Mac
+docker run -it -v /path/to/your/data:/app/data sacdalance/thesis-aigvdet:gpu bash
+
+# With GPU support
+docker run --gpus all -it -v /path/to/data:/app/data sacdalance/thesis-aigvdet:gpu bash
+```
+
+## Local Development with venv
+```bash
+# Activate venv
+source .venv/bin/activate # Linux/Mac
+.\.venv\Scripts\Activate.ps1 # Windows
+
+# Install dependencies
+uv pip install -e .
+
+# Set data path
+export AIGVDET_DATA_PATH="/path/to/your/data" # Linux/Mac
+$env:AIGVDET_DATA_PATH = "C:\path\to\your\data" # Windows
+```
+
+## Running the Paper
+
+### Training
+```bash
+# Basic training command
+python train.py --gpus 0 --exp_name TRAIN_RGB_BRANCH datasets RGB_TRAINSET datasets_test RGB_TESTSET
+
+# For optical flow branch
+python train.py --gpus 0 --exp_name TRAIN_OF_BRANCH datasets OpticalFlow_TRAINSET datasets_test OpticalFlow_TESTSET
+
+# Using the provided script
+./train.sh
+```
+
+### Testing on Dataset
+```bash
+python test.py \
+ -fop "data/test/hotshot" \
+ -mop "checkpoints/optical_aug.pth" \
+ -for "data/test/original/hotshot" \
+ -mor "checkpoints/original_aug.pth" \
+ -e "data/results/T2V/hotshot.csv" \
+ -ef "data/results/frame/T2V/hotshot.csv" \
+ -t 0.5
+```
+
+### Demo on Video
+```bash
+python demo.py \
+ --path "demo_video/fake_sora/video.mp4" \
+ --folder_original_path "frame/000000" \
+ --folder_optical_flow_path "optical_result/000000" \
+ -mop "checkpoints/optical.pth" \
+ -mor "checkpoints/original.pth"
+```
+
+### Docker Examples
+```bash
+# Training with Docker
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ sacdalance/thesis-aigvdet:gpu \
+ python3.11 train.py --gpus 0 --exp_name TRAIN_RGB datasets RGB_TRAINSET datasets_test RGB_TESTSET
+
+# Testing with Docker
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ sacdalance/thesis-aigvdet:gpu \
+ python3.11 test.py \
+ -fop "data/test/hotshot" \
+ -mop "checkpoints/optical_aug.pth" \
+ -for "data/test/original/hotshot" \
+ -mor "checkpoints/original_aug.pth" \
+ -e "data/results/T2V/hotshot.csv"
+
+# Demo with Docker
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/raft_model:/app/raft_model \
+ -v $(pwd)/demo_video:/app/demo_video \
+ sacdalance/thesis-aigvdet:gpu \
+ python3.11 demo.py \
+ --path "demo_video/fake_sora/video.mp4" \
+ --folder_original_path "frame/000000" \
+ --folder_optical_flow_path "optical_result/000000" \
+ -mop "checkpoints/optical.pth" \
+ -mor "checkpoints/original.pth"
+```
+
+## 🚀 Cross-Device Deployment Guide
+
+### Option 1: Using Pre-built Docker Images (Recommended)
+
+#### Prerequisites
+```bash
+# Install Docker
+# Windows: Download Docker Desktop from docker.com
+# Linux: sudo apt install docker.io docker-compose
+# Mac: brew install docker docker-compose
+
+# For GPU support (Linux/WSL2)
+sudo apt install nvidia-docker2
+sudo systemctl restart docker
+```
+
+#### Quick Setup on New Device
+```bash
+# 1. Clone the repository
+git clone https://github.com/sacdalance/AIGVDet.git
+cd AIGVDet # You are now INSIDE the repo folder
+
+# 2. Create data directories INSIDE the repo
+mkdir -p data/train data/test data/results checkpoints raft_model demo_video
+
+# 3. Copy your downloaded data INTO the repo folders (REQUIRED BEFORE RUNNING):
+# - Training data → ./data/train/ (inside the AIGVDet repo)
+# - Test videos → ./data/test/ (inside the AIGVDet repo)
+# - Model weights → ./checkpoints/ (inside the AIGVDet repo)
+# - RAFT model → ./raft_model/ (inside the AIGVDet repo)
+# - Demo videos → ./demo_video/ (inside the AIGVDet repo)
+
+# 4. Pull and run (GPU version) - ONLY AFTER DATA IS COPIED
+docker pull sacdalance/thesis-aigvdet:gpu
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/raft_model:/app/raft_model \
+ -v $(pwd)/demo_video:/app/demo_video \
+ sacdalance/thesis-aigvdet:gpu bash
+
+# 5. Inside container, run your experiments
+python3.11 train.py --gpus 0 --exp_name my_experiment
+```
+
+#### Windows PowerShell Version
+```powershell
+# Clone and setup on new device
+git clone https://github.com/sacdalance/AIGVDet.git
+cd AIGVDet
+mkdir data\train, data\test, data\results, checkpoints, raft_model, demo_video
+
+# IMPORTANT: Copy your data to these folders FIRST, then:
+docker pull sacdalance/thesis-aigvdet:gpu
+docker run --gpus all -it --rm `
+ -v ${PWD}/data:/app/data `
+ -v ${PWD}/checkpoints:/app/checkpoints `
+ -v ${PWD}/raft_model:/app/raft_model `
+ -v ${PWD}/demo_video:/app/demo_video `
+ sacdalance/thesis-aigvdet:gpu bash
+```
+
+### Option 2: Local Python Environment
+
+#### Prerequisites
+```bash
+# Install Python 3.11
+# Windows: Download from python.org
+# Linux: sudo apt install python3.11 python3.11-venv
+# Mac: brew install python@3.11
+
+# Install uv (fast Python package manager)
+curl -LsSf https://astral.sh/uv/install.sh | sh # Linux/Mac
+# Windows: iwr https://astral.sh/uv/install.ps1 | iex
+```
+
+#### Setup Steps
+```bash
+# 1. Clone repository on new device
+git clone https://github.com/sacdalance/AIGVDet.git
+cd AIGVDet
+
+# 2. Create virtual environment and install dependencies
+uv venv --python 3.11
+source .venv/bin/activate # Linux/Mac
+# .\.venv\Scripts\Activate.ps1 # Windows
+
+# 3. Install project
+uv pip install -e .
+
+# 4. Copy your data to appropriate folders:
+# - data/train/ (training frames)
+# - data/test/ (test videos)
+# - checkpoints/ (model weights)
+# - raft_model/ (RAFT weights)
+# - demo_video/ (demo videos)
+
+# 5. Run experiments
+python train.py --gpus 0 --exp_name my_experiment
+python test.py -fop "data/test/hotshot" -mop "checkpoints/optical_aug.pth" ...
+python demo.py --path "demo_video/video.mp4" ...
+```
+
+### 📁 Expected Folder Structure After Setup
+```
+AIGVDet/ # ← This is your cloned GitHub repo
+├── pyproject.toml # ← Project files from GitHub
+├── train.py # ← Python scripts from GitHub
+├── test.py # ← Python scripts from GitHub
+├── demo.py # ← Python scripts from GitHub
+├── core/ # ← Code folders from GitHub
+├── networks/ # ← Code folders from GitHub
+├── data/ # ← YOU CREATE & FILL THIS
+│ ├── train/ # ← Copy training data here
+│ │ └── trainset_1/
+│ │ ├── 0_real/
+│ │ └── 1_fake/
+│ ├── test/ # ← Copy test videos here
+│ └── results/ # ← Output results go here
+├── checkpoints/ # ← YOU CREATE & FILL THIS
+│ ├── optical_aug.pth # ← Copy model weights here
+│ └── original_aug.pth
+├── raft_model/ # ← YOU CREATE & FILL THIS
+│ └── raft-things.pth # ← Copy RAFT model here
+└── demo_video/ # ← YOU CREATE & FILL THIS
+ ├── fake_sora/ # ← Copy demo videos here
+ └── real/
+```
+
+**Key Point:** Everything goes INSIDE the AIGVDet folder you cloned from GitHub!
+
+**Note:** The data folders (`data/`, `checkpoints/`, `raft_model/`, `demo_video/`) are automatically ignored by git (see `.gitignore`), so your large data files won't be committed to GitHub.
+
+## 🔄 Updating Docker Images
+
+### If you need to rebuild the Docker image (e.g., after dependency changes):
+
+#### Build locally:
+```bash
+# Build GPU version
+docker build -f Dockerfile.gpu -t sacdalance/thesis-aigvdet:gpu .
+
+# Build CPU version
+docker build -f Dockerfile.cpu -t sacdalance/thesis-aigvdet:cpu .
+
+# Test the build
+docker run --rm sacdalance/thesis-aigvdet:gpu python3.11 --version
+```
+
+#### Push to Docker Hub (for maintainers):
+```bash
+# Login to Docker Hub
+docker login
+
+# Push updated images
+docker push sacdalance/thesis-aigvdet:gpu
+docker push sacdalance/thesis-aigvdet:cpu
+
+# Or use the provided scripts
+./push-docker.sh # Linux/Mac
+./push-docker.ps1 # Windows
+```
+
+#### For users - pull latest updates:
+```bash
+# Pull latest version
+docker pull sacdalance/thesis-aigvdet:gpu
+
+# Force rebuild without cache if needed
+docker build --no-cache -f Dockerfile.gpu -t sacdalance/thesis-aigvdet:gpu .
+```
+
+### 📦 Data Transfer to New Device
+
+Since you've already downloaded all the data, you need to transfer it to your new device:
+
+#### Option 1: Cloud Storage (Recommended)
+```bash
+# Upload from current device to cloud (Google Drive, OneDrive, etc.)
+# Then download on new device to the appropriate folders
+
+# Or use cloud sync folders:
+# 1. Put data in cloud sync folder on current device
+# 2. Access from new device once synced
+```
+
+#### Option 2: External Drive/USB
+```bash
+# Copy from current device to external drive:
+# - data/ folder (training + test data)
+# - checkpoints/ folder (model weights)
+# - raft_model/ folder (RAFT weights)
+# - demo_video/ folder (demo videos)
+
+# Then copy to new device after cloning repo
+```
+
+#### Option 3: Network Transfer
+```bash
+# If both devices are on same network:
+# Use scp, rsync, or network sharing to transfer data folders
+```
+
+### 🔧 Troubleshooting
+
+#### GPU Issues
+```bash
+# Check GPU availability
+nvidia-smi
+docker run --gpus all nvidia/cuda:11.7-base nvidia-smi
+
+# If GPU not detected in Docker:
+# 1. Install nvidia-docker2
+# 2. Restart Docker service
+# 3. Use --gpus all flag
+```
+
+#### Permission Issues (Linux)
+```bash
+# Add user to docker group
+sudo usermod -aG docker $USER
+# Logout and login again
+```
+
+#### Memory Issues
+```bash
+# For large datasets, increase Docker memory limit
+# Docker Desktop → Settings → Resources → Memory → 8GB+
+```
+
+### 📋 Quick Deployment Checklist (IN ORDER!)
+- [ ] Docker installed (with GPU support if needed)
+- [ ] Repository cloned: `git clone https://github.com/sacdalance/AIGVDet.git`
+- [ ] Data folders created: `mkdir data/train data/test checkpoints raft_model demo_video`
+- [ ] **DATA TRANSFER COMPLETE** ⚠️ (Required before Docker run):
+ - [ ] Training data copied to `./data/train/`
+ - [ ] Test videos copied to `./data/test/`
+ - [ ] Model weights copied to `./checkpoints/`
+ - [ ] RAFT model copied to `./raft_model/`
+ - [ ] Demo videos copied to `./demo_video/`
+- [ ] Docker image pulled: `docker pull sacdalance/thesis-aigvdet:gpu`
+- [ ] Test run: `docker run --gpus all -it sacdalance/thesis-aigvdet:gpu python3.11 --version`
+
+**⚠️ CRITICAL:** The `-v $(pwd)/data:/app/data` flag mounts your local folders into the container. If the data isn't there, the container will see empty folders!
+
+## Notes
+- **Research use only**: The datasets are only allowed for research purposes
+- **Citation required**: If you use this work, please cite the PRCV 2024 paper
+- **Contact**: For questions, contact lyan924@cuc.edu.cn
+- **Docker Hub**: Pre-built images available at `sacdalance/thesis-aigvdet:gpu` and `sacdalance/thesis-aigvdet:cpu`
\ No newline at end of file
diff --git a/md-files/DOCKER_COMPARISON.md b/md-files/DOCKER_COMPARISON.md
new file mode 100644
index 0000000..35cf865
--- /dev/null
+++ b/md-files/DOCKER_COMPARISON.md
@@ -0,0 +1,200 @@
+# Docker Configuration Comparison
+
+## Quick Comparison
+
+| Feature | CPU Version | GPU Version |
+|---------|------------|-------------|
+| **Base Image** | python:3.11-slim | nvidia/cuda:11.7.1-cudnn8-runtime |
+| **Image Size** | ~2-3 GB | ~8-10 GB |
+| **PyTorch** | 2.0.0+cpu | 2.0.0+cu117 |
+| **Build Time** | ~5-10 min | ~10-20 min |
+| **Training Speed** | Slow (baseline) | 10-100x faster |
+| **Memory Required** | 4-8 GB RAM | 8+ GB RAM + GPU VRAM |
+| **Hardware Required** | Any CPU | NVIDIA GPU + nvidia-docker |
+| **Use Case** | Testing, inference on small data | Training, production inference |
+| **Cost** | Low | High (GPU required) |
+
+## When to Use CPU Version
+
+✅ **Good for:**
+- Initial testing and development
+- Small-scale inference
+- Environments without GPU access
+- Budget-constrained deployments
+- CI/CD pipelines for testing
+
+❌ **Not recommended for:**
+- Training large models
+- Processing high-resolution videos
+- Batch processing many videos
+- Production workloads with time constraints
+
+## When to Use GPU Version
+
+✅ **Good for:**
+- Training models
+- Large-scale inference
+- Video processing pipelines
+- Production deployments
+- Research and experimentation
+
+❌ **Not recommended for:**
+- Simple testing
+- Environments without NVIDIA GPU
+- Cost-sensitive deployments where speed isn't critical
+
+## Resource Requirements
+
+### CPU Version
+```yaml
+Minimum:
+ RAM: 4 GB
+ Storage: 10 GB
+ CPU: 2 cores
+
+Recommended:
+ RAM: 8 GB
+ Storage: 20 GB
+ CPU: 4+ cores
+```
+
+### GPU Version
+```yaml
+Minimum:
+ RAM: 8 GB
+ GPU VRAM: 6 GB
+ Storage: 20 GB
+ GPU: NVIDIA GPU with CUDA 11.7 support
+
+Recommended:
+ RAM: 16 GB
+ GPU VRAM: 12+ GB
+ Storage: 50 GB
+ GPU: NVIDIA RTX 3080 or better
+```
+
+## Performance Comparison
+
+### Training (1000 iterations)
+| Hardware | Time | Relative Speed |
+|----------|------|----------------|
+| CPU (Intel i7) | ~4-6 hours | 1x |
+| GPU (GTX 1080 Ti) | ~20-30 min | 10-15x |
+| GPU (RTX 3090) | ~10-15 min | 20-30x |
+| GPU (A100) | ~5-8 min | 40-60x |
+
+### Inference (Single Video)
+| Hardware | Time | Relative Speed |
+|----------|------|----------------|
+| CPU (Intel i7) | ~5-10 min | 1x |
+| GPU (GTX 1080 Ti) | ~30-60 sec | 5-10x |
+| GPU (RTX 3090) | ~15-30 sec | 10-20x |
+| GPU (A100) | ~10-20 sec | 15-30x |
+
+*Note: Times vary based on video resolution, length, and model complexity*
+
+## Build Time Comparison
+
+### Initial Build
+| Version | Download Size | Build Time |
+|---------|--------------|------------|
+| CPU | ~500 MB | 5-10 min |
+| GPU | ~2-3 GB | 10-20 min |
+
+### Rebuild (after changes)
+| Version | Time |
+|---------|------|
+| CPU | 1-2 min |
+| GPU | 2-5 min |
+
+## Docker Compose Configuration
+
+The included `docker-compose.yml` is configured with optimal defaults:
+
+### CPU Container
+```yaml
+Resources:
+ - 4 GB shared memory
+ - All CPU cores available
+ - Port 6007 for TensorBoard
+```
+
+### GPU Container
+```yaml
+Resources:
+ - 8 GB shared memory
+ - All NVIDIA GPUs available
+ - Port 6006 for TensorBoard
+ - CUDA environment configured
+```
+
+## Cost Comparison (Cloud Deployment)
+
+### AWS EC2 Approximate Hourly Costs
+| Instance Type | Specs | Cost/Hour | Best For |
+|--------------|-------|-----------|----------|
+| t3.2xlarge | 8 vCPU, 32 GB RAM | $0.33 | CPU Testing |
+| p3.2xlarge | V100 GPU, 16 GB VRAM | $3.06 | GPU Training |
+| p4d.24xlarge | 8x A100 GPUs | $32.77 | Large-scale Training |
+
+*Prices as of 2024, may vary by region*
+
+## Recommendations by Use Case
+
+### Research & Development
+- **Start with**: GPU version
+- **Why**: Faster iteration, better for experimentation
+- **Fallback**: CPU for quick tests
+
+### Production Inference
+- **Small scale**: CPU version
+- **Large scale**: GPU version
+- **Why**: Cost vs. speed tradeoff
+
+### CI/CD Testing
+- **Use**: CPU version
+- **Why**: No GPU required, faster builds, lower cost
+
+### Training
+- **Always use**: GPU version
+- **Why**: CPU training is impractically slow
+
+## Environment Variables
+
+Both versions support these environment variables:
+
+```bash
+# PyTorch settings
+CUDA_VISIBLE_DEVICES=0,1 # GPU only: Select GPUs
+OMP_NUM_THREADS=4 # CPU only: Thread count
+
+# Application settings
+PYTHONUNBUFFERED=1 # Real-time logging
+PYTHONDONTWRITEBYTECODE=1 # Smaller image size
+
+# UV package manager
+UV_SYSTEM_PYTHON=1 # Use system Python
+```
+
+## Summary
+
+Choose your version based on:
+
+1. **Hardware Available**
+ - No GPU? → CPU version
+ - Have GPU? → GPU version
+
+2. **Use Case**
+ - Testing/Development? → Start with CPU, move to GPU
+ - Training? → GPU version mandatory
+ - Production inference? → Depends on scale
+
+3. **Budget**
+ - Limited? → CPU version
+ - Performance critical? → GPU version
+
+4. **Time Constraints**
+ - Quick results needed? → GPU version
+ - Can wait? → CPU version acceptable
+
+For most users working on AI-Generated Video Detection, the **GPU version is recommended** for any serious work beyond initial exploration.
diff --git a/md-files/DOCKER_INDEX.md b/md-files/DOCKER_INDEX.md
new file mode 100644
index 0000000..f2b125a
--- /dev/null
+++ b/md-files/DOCKER_INDEX.md
@@ -0,0 +1,199 @@
+# AIGVDet Docker Documentation Index
+
+Welcome to the AIGVDet Docker implementation! This project now supports containerized deployment with both CPU and GPU configurations.
+
+## 📚 Documentation Overview
+
+### Quick Start
+- **[DOCKER_QUICKREF.md](DOCKER_QUICKREF.md)** - Quick reference for common Docker commands
+ - Build commands
+ - Run commands
+ - Common tasks
+ - One-line examples
+
+### Detailed Guides
+- **[DOCKER_USAGE.md](DOCKER_USAGE.md)** - Comprehensive usage guide
+ - Prerequisites and setup
+ - Detailed build instructions
+ - Training and testing examples
+ - Troubleshooting
+ - Volume management
+ - TensorBoard access
+
+- **[DOCKER_COMPARISON.md](DOCKER_COMPARISON.md)** - CPU vs GPU comparison
+ - Performance benchmarks
+ - Cost analysis
+ - Resource requirements
+ - Use case recommendations
+
+### Main Documentation
+- **[README.md](README.md)** - Project overview and setup (now includes Docker section)
+- **[MIGRATION_SUMMARY.md](MIGRATION_SUMMARY.md)** - UV migration details
+
+## 🎯 Getting Started in 3 Steps
+
+### 1. Choose Your Version
+- **GPU** - For training and fast inference (requires NVIDIA GPU + nvidia-docker)
+- **CPU** - For testing and CPU-only environments
+
+### 2. Build the Image
+
+**Windows (PowerShell):**
+```powershell
+.\build-docker.ps1 gpu # or 'cpu'
+```
+
+**Linux/Mac:**
+```bash
+./build-docker.sh gpu # or 'cpu'
+```
+
+**Using Make:**
+```bash
+make build-gpu # or 'build-cpu'
+```
+
+### 3. Run the Container
+
+**With Docker Compose (Recommended):**
+```bash
+docker-compose up aigvdet-gpu
+```
+
+**Direct Docker Run:**
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ aigvdet:gpu
+```
+
+## 📁 File Structure
+
+```
+AIGVDet/
+├── 🐳 Docker Configuration
+│ ├── Dockerfile.gpu # GPU version
+│ ├── Dockerfile.cpu # CPU version
+│ ├── docker-compose.yml # Compose configuration
+│ └── .dockerignore # Build exclusions
+│
+├── 🔧 Build Scripts
+│ ├── build-docker.ps1 # Windows script
+│ ├── build-docker.sh # Linux/Mac script
+│ └── Makefile # Make targets
+│
+├── 📖 Documentation
+│ ├── DOCKER_INDEX.md # This file
+│ ├── DOCKER_QUICKREF.md # Quick reference
+│ ├── DOCKER_USAGE.md # Detailed guide
+│ ├── DOCKER_COMPARISON.md # CPU vs GPU
+│ └── README.md # Main README
+│
+└── 🐍 Project Files
+ ├── pyproject.toml # UV project config
+ ├── core/ # Core modules
+ ├── networks/ # Network definitions
+ ├── train.py # Training script
+ ├── test.py # Testing script
+ └── demo.py # Demo script
+```
+
+## 🎓 Common Use Cases
+
+### Training a Model
+```bash
+# GPU training
+docker-compose run aigvdet-gpu python3.11 train.py \
+ --gpus 0 \
+ --exp_name TRAIN_RGB \
+ datasets RGB_TRAINSET \
+ datasets_test RGB_TESTSET
+```
+
+### Running Tests
+```bash
+# Test on dataset
+docker-compose run aigvdet-gpu python3.11 test.py \
+ -fop "data/test/hotshot" \
+ -mop "checkpoints/optical_aug.pth" \
+ -for "data/test/original/hotshot" \
+ -mor "checkpoints/original_aug.pth" \
+ -e "results/hotshot.csv"
+```
+
+### Demo on Video
+```bash
+# Process a single video
+docker-compose run aigvdet-gpu python3.11 demo.py \
+ --path "demo_video/video.mp4" \
+ --folder_original_path "frame/output" \
+ --folder_optical_flow_path "optical_result/output" \
+ -mop "checkpoints/optical.pth" \
+ -mor "checkpoints/original.pth"
+```
+
+### Interactive Development
+```bash
+# Open bash shell in container
+docker-compose run aigvdet-gpu /bin/bash
+
+# Or using Make
+make shell-gpu
+```
+
+## 🔍 Quick Command Reference
+
+| Task | Command |
+|------|---------|
+| Build GPU | `.\build-docker.ps1 gpu` or `make build-gpu` |
+| Build CPU | `.\build-docker.ps1 cpu` or `make build-cpu` |
+| Run GPU | `docker-compose up aigvdet-gpu` |
+| Run CPU | `docker-compose up aigvdet-cpu` |
+| Shell GPU | `make shell-gpu` |
+| Shell CPU | `make shell-cpu` |
+| Clean up | `make clean` |
+| View logs | `docker-compose logs -f` |
+
+## 🆘 Need Help?
+
+1. **Quick commands?** → [DOCKER_QUICKREF.md](DOCKER_QUICKREF.md)
+2. **Detailed setup?** → [DOCKER_USAGE.md](DOCKER_USAGE.md)
+3. **CPU or GPU?** → [DOCKER_COMPARISON.md](DOCKER_COMPARISON.md)
+4. **Project info?** → [README.md](README.md)
+
+## 💡 Tips
+
+- **First time?** Start with [DOCKER_QUICKREF.md](DOCKER_QUICKREF.md)
+- **Troubleshooting?** Check [DOCKER_USAGE.md](DOCKER_USAGE.md) troubleshooting section
+- **Performance tuning?** See [DOCKER_COMPARISON.md](DOCKER_COMPARISON.md)
+- **Windows user?** All PowerShell commands are in the guides
+
+## 🎯 What's Different from Regular Installation?
+
+| Aspect | Regular Setup | Docker Setup |
+|--------|--------------|--------------|
+| Installation | Manual dependencies | One `docker build` command |
+| Reproducibility | Varies by system | Identical everywhere |
+| GPU Setup | Manual CUDA install | Pre-configured in image |
+| Isolation | System-wide packages | Containerized environment |
+| Portability | Machine-specific | Runs anywhere Docker runs |
+| Cleanup | Manual uninstall | `docker rmi` |
+
+## 🚀 Ready to Start?
+
+1. Pick your documentation:
+ - **Quick start** → [DOCKER_QUICKREF.md](DOCKER_QUICKREF.md)
+ - **Full guide** → [DOCKER_USAGE.md](DOCKER_USAGE.md)
+
+2. Choose your version:
+ - **GPU** → For training and production
+ - **CPU** → For testing and development
+
+3. Build and run:
+ ```bash
+ .\build-docker.ps1 gpu
+ docker-compose up aigvdet-gpu
+ ```
+
+Happy containerizing! 🐳
diff --git a/md-files/DOCKER_QUICKREF.md b/md-files/DOCKER_QUICKREF.md
new file mode 100644
index 0000000..a50b348
--- /dev/null
+++ b/md-files/DOCKER_QUICKREF.md
@@ -0,0 +1,182 @@
+# AIGVDet Docker Quick Reference
+
+## Pull from Docker Hub
+
+```bash
+# Pull GPU version
+docker pull sacdalance/thesis-aigvdet:gpu
+
+# Pull CPU version
+docker pull sacdalance/thesis-aigvdet:cpu
+```
+
+## Build Images
+
+### Windows (PowerShell)
+```powershell
+# Build both
+.\build-docker.ps1 all
+
+# Build GPU only
+.\build-docker.ps1 gpu
+
+# Build CPU only
+.\build-docker.ps1 cpu
+```
+
+### Linux/Mac (Bash)
+```bash
+# Build both
+./build-docker.sh all
+
+# Build GPU only
+./build-docker.sh gpu
+
+# Build CPU only
+./build-docker.sh cpu
+```
+
+## Run with Docker Compose (Recommended)
+
+```bash
+# GPU version
+docker-compose up aigvdet-gpu
+
+# CPU version
+docker-compose up aigvdet-cpu
+
+# Build and run
+docker-compose up --build aigvdet-gpu
+
+# Run in background
+docker-compose up -d aigvdet-gpu
+```
+
+## Run with Docker Run
+
+### GPU Version (Windows PowerShell)
+```powershell
+docker run --gpus all -it --rm `
+ -v ${PWD}/data:/app/data `
+ -v ${PWD}/checkpoints:/app/checkpoints `
+ -v ${PWD}/raft_model:/app/raft_model `
+ -p 6006:6006 `
+ sacdalance/thesis-aigvdet:gpu `
+ python3.11 train.py --gpus 0 --exp_name my_experiment
+```
+
+### CPU Version (Windows PowerShell)
+```powershell
+docker run -it --rm `
+ -v ${PWD}/data:/app/data `
+ -v ${PWD}/checkpoints:/app/checkpoints `
+ -p 6006:6006 `
+ sacdalance/thesis-aigvdet:cpu `
+ python train.py --exp_name my_experiment
+```
+
+### GPU Version (Linux/Mac)
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/raft_model:/app/raft_model \
+ -p 6006:6006 \
+ sacdalance/thesis-aigvdet:gpu \
+ python3.11 train.py --gpus 0 --exp_name my_experiment
+```
+
+## Common Tasks
+
+### Training RGB Branch
+```bash
+docker-compose run aigvdet-gpu python3.11 train.py --gpus 0 --exp_name TRAIN_RGB datasets RGB_TRAINSET datasets_test RGB_TESTSET
+```
+
+### Training Optical Flow Branch
+```bash
+docker-compose run aigvdet-gpu python3.11 train.py --gpus 0 --exp_name TRAIN_OF datasets OpticalFlow_TRAINSET datasets_test OpticalFlow_TESTSET
+```
+
+### Testing
+```bash
+docker-compose run aigvdet-gpu python3.11 test.py \
+ -fop "data/test/hotshot" \
+ -mop "checkpoints/optical_aug.pth" \
+ -for "data/test/original/hotshot" \
+ -mor "checkpoints/original_aug.pth" \
+ -e "data/results/T2V/hotshot.csv" \
+ -ef "data/results/frame/T2V/hotshot.csv" \
+ -t 0.5
+```
+
+### Demo on Video
+```bash
+docker-compose run aigvdet-gpu python3.11 demo.py \
+ --path "demo_video/video.mp4" \
+ --folder_original_path "frame/000000" \
+ --folder_optical_flow_path "optical_result/000000" \
+ -mop "checkpoints/optical.pth" \
+ -mor "checkpoints/original.pth"
+```
+
+### Interactive Shell
+```bash
+# GPU
+docker-compose run aigvdet-gpu /bin/bash
+
+# CPU
+docker-compose run aigvdet-cpu /bin/bash
+```
+
+### TensorBoard
+```bash
+# Access at http://localhost:6006
+docker-compose up aigvdet-gpu
+```
+
+## Cleanup
+
+```bash
+# Stop containers
+docker-compose down
+
+# Remove images
+docker rmi aigvdet:gpu aigvdet:cpu
+
+# Full cleanup
+docker system prune -a
+```
+
+## File Structure
+
+```
+AIGVDet/
+├── Dockerfile.gpu # GPU version Dockerfile
+├── Dockerfile.cpu # CPU version Dockerfile
+├── docker-compose.yml # Orchestration config
+├── .dockerignore # Files to exclude from build
+├── build-docker.sh # Linux/Mac build script
+├── build-docker.ps1 # Windows build script
+├── DOCKER_USAGE.md # Detailed usage guide
+└── DOCKER_QUICKREF.md # This file
+```
+
+## Volumes Mounted
+
+- `./data` → `/app/data` - Training/test data
+- `./checkpoints` → `/app/checkpoints` - Model weights
+- `./raft_model` → `/app/raft_model` - RAFT model
+- `./logs` → `/app/logs` - Training logs
+
+## Ports Exposed
+
+- `6006` - TensorBoard (GPU container)
+- `6007` - TensorBoard (CPU container, in compose)
+
+## Image Details
+
+| Version | Base Image | Size | Python | PyTorch | CUDA |
+|---------|-----------|------|--------|---------|------|
+| GPU | nvidia/cuda:11.7.1-cudnn8-runtime | ~8-10GB | 3.11 | 2.0.0+cu117 | 11.7 |
+| CPU | python:3.11-slim | ~2-3GB | 3.11 | 2.0.0+cpu | N/A |
diff --git a/md-files/DOCKER_USAGE.md b/md-files/DOCKER_USAGE.md
new file mode 100644
index 0000000..66ffc2a
--- /dev/null
+++ b/md-files/DOCKER_USAGE.md
@@ -0,0 +1,237 @@
+# Docker Usage Guide for AIGVDet
+
+This guide explains how to build and run AIGVDet using Docker with both CPU and GPU support.
+
+## Prerequisites
+
+### For CPU Version
+- Docker installed
+- At least 8GB RAM recommended
+
+### For GPU Version
+- Docker installed
+- NVIDIA Docker runtime (nvidia-docker2)
+- NVIDIA GPU with CUDA support
+- NVIDIA drivers installed
+
+## Quick Start
+
+### Build Docker Images
+
+**Build GPU version:**
+```bash
+docker build -f Dockerfile.gpu -t aigvdet:gpu .
+```
+
+**Build CPU version:**
+```bash
+docker build -f Dockerfile.cpu -t aigvdet:cpu .
+```
+
+### Run Containers
+
+**Run GPU version:**
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/raft_model:/app/raft_model \
+ -p 6006:6006 \
+ aigvdet:gpu
+```
+
+**Run CPU version:**
+```bash
+docker run -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/raft_model:/app/raft_model \
+ -p 6006:6006 \
+ aigvdet:cpu
+```
+
+## Using Docker Compose
+
+Docker Compose simplifies running the containers with all necessary configurations.
+
+**Start GPU service:**
+```bash
+docker-compose up aigvdet-gpu
+```
+
+**Start CPU service:**
+```bash
+docker-compose up aigvdet-cpu
+```
+
+**Build and start:**
+```bash
+docker-compose up --build aigvdet-gpu
+```
+
+## Training Examples
+
+### GPU Training - RGB Branch
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ aigvdet:gpu \
+ python3.11 train.py --gpus 0 --exp_name TRAIN_RGB_BRANCH \
+ datasets RGB_TRAINSET datasets_test RGB_TESTSET
+```
+
+### GPU Training - Optical Flow Branch
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ aigvdet:gpu \
+ python3.11 train.py --gpus 0 --exp_name TRAIN_OF_BRANCH \
+ datasets OpticalFlow_TRAINSET datasets_test OpticalFlow_TESTSET
+```
+
+### CPU Training
+```bash
+docker run -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ aigvdet:cpu \
+ python train.py --exp_name TRAIN_RGB_BRANCH \
+ datasets RGB_TRAINSET datasets_test RGB_TESTSET
+```
+
+## Testing
+
+### Test on Dataset
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ aigvdet:gpu \
+ python3.11 test.py \
+ -fop "data/test/hotshot" \
+ -mop "checkpoints/optical_aug.pth" \
+ -for "data/test/original/hotshot" \
+ -mor "checkpoints/original_aug.pth" \
+ -e "data/results/T2V/hotshot.csv" \
+ -ef "data/results/frame/T2V/hotshot.csv" \
+ -t 0.5
+```
+
+## Demo on Video
+
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/raft_model:/app/raft_model \
+ -v $(pwd)/demo_video:/app/demo_video \
+ aigvdet:gpu \
+ python3.11 demo.py \
+ --path "demo_video/fake_sora/video.mp4" \
+ --folder_original_path "frame/000000" \
+ --folder_optical_flow_path "optical_result/000000" \
+ -mop "checkpoints/optical.pth" \
+ -mor "checkpoints/original.pth"
+```
+
+## TensorBoard
+
+Access TensorBoard by visiting `http://localhost:6006` in your browser after starting the container.
+
+To explicitly run TensorBoard:
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/logs:/app/logs \
+ -p 6006:6006 \
+ aigvdet:gpu \
+ tensorboard --logdir=/app/logs --host=0.0.0.0 --port=6006
+```
+
+## Interactive Shell
+
+**GPU container:**
+```bash
+docker run --gpus all -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ aigvdet:gpu \
+ /bin/bash
+```
+
+**CPU container:**
+```bash
+docker run -it --rm \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ aigvdet:cpu \
+ /bin/bash
+```
+
+## Volume Mounts Explained
+
+- `-v $(pwd)/data:/app/data` - Mount your training/test data
+- `-v $(pwd)/checkpoints:/app/checkpoints` - Mount model checkpoints
+- `-v $(pwd)/raft_model:/app/raft_model` - Mount RAFT model weights
+- `-v $(pwd)/logs:/app/logs` - Mount training logs for TensorBoard
+
+## PowerShell Commands (Windows)
+
+On Windows PowerShell, use `${PWD}` instead of `$(pwd)`:
+
+```powershell
+docker run --gpus all -it --rm `
+ -v ${PWD}/data:/app/data `
+ -v ${PWD}/checkpoints:/app/checkpoints `
+ -v ${PWD}/raft_model:/app/raft_model `
+ -p 6006:6006 `
+ aigvdet:gpu
+```
+
+## Troubleshooting
+
+### GPU not detected
+Ensure nvidia-docker2 is installed:
+```bash
+# Install nvidia-docker2
+sudo apt-get install nvidia-docker2
+sudo systemctl restart docker
+
+# Verify
+docker run --rm --gpus all nvidia/cuda:11.7.1-base-ubuntu22.04 nvidia-smi
+```
+
+### Out of memory
+Increase shared memory:
+```bash
+docker run --gpus all -it --rm --shm-size=8g ...
+```
+
+### Permission issues
+If you encounter permission issues with mounted volumes:
+```bash
+docker run --gpus all -it --rm --user $(id -u):$(id -g) ...
+```
+
+## Image Sizes
+
+- GPU image: ~8-10GB (includes CUDA runtime)
+- CPU image: ~2-3GB (lighter weight)
+
+## Cleaning Up
+
+Remove containers:
+```bash
+docker-compose down
+```
+
+Remove images:
+```bash
+docker rmi aigvdet:gpu aigvdet:cpu
+```
+
+Clean up all stopped containers and unused images:
+```bash
+docker system prune -a
+```
diff --git a/md-files/DOCKER_VM_SETUP.md b/md-files/DOCKER_VM_SETUP.md
new file mode 100644
index 0000000..f28e112
--- /dev/null
+++ b/md-files/DOCKER_VM_SETUP.md
@@ -0,0 +1,186 @@
+# Docker Setup Guide for VM
+
+## Quick Start
+
+### 1. Automated Data Deployment (Recommended for Vast.ai)
+Since the dataset is large (27GB+), we use a startup script to download it directly to the VM.
+
+1. **Start your instance** (Vast.ai, AWS, etc.).
+2. **Open a terminal** in the container.
+3. **Run the download script**:
+ ```bash
+ # This will download the dataset from Google Drive and unzip it
+ chmod +x download_data.sh
+ ./download_data.sh
+ ```
+ *Note: The script defaults to the project's Google Drive ID. You can pass a different ID if needed: `./download_data.sh `*
+
+### 2. Manual Data Setup (Local Development)
+If you are running locally or want to set up manually:
+
+```bash
+# Run the setup script
+python setup_data_structure.py
+
+# Or manually create:
+mkdir -p data/train/trainset_1/0_real/video_00000
+mkdir -p data/train/trainset_1/1_fake/video_00000
+mkdir -p data/val/val_set_1/0_real/video_00000
+mkdir -p data/val/val_set_1/1_fake/video_00000
+```
+
+### 2. Add Your Training Data
+Place your extracted frames in:
+- `data/train/trainset_1/0_real/` - Real video frames
+- `data/train/trainset_1/1_fake/` - Fake video frames
+
+Each video should be in its own directory with frames named sequentially:
+```
+data/train/trainset_1/0_real/
+├── video_00000/
+│ ├── 00000.png
+│ ├── 00001.png
+│ └── ...
+├── video_00001/
+│ └── ...
+```
+
+### 3. Ensure .env File Exists
+Your `.env` file should contain your wandb API key:
+```bash
+WANDB="your_api_key_here"
+```
+
+### 4. Run with Docker
+
+#### Using docker-compose (Recommended):
+```bash
+# GPU version
+docker-compose up aigvdet-gpu
+
+# CPU version
+docker-compose up aigvdet-cpu
+```
+
+#### Using docker run:
+```bash
+# GPU version
+docker run --gpus all \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/.env:/app/.env:ro \
+ --env-file .env \
+ sacdalance/thesis-aigvdet:gpu \
+ python train.py --exp_name my_experiment
+
+# CPU version
+docker run \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/.env:/app/.env:ro \
+ --env-file .env \
+ sacdalance/thesis-aigvdet:cpu \
+ python train.py --exp_name my_experiment
+```
+
+#### On Windows PowerShell:
+```powershell
+# GPU version
+docker run --gpus all `
+ -v ${PWD}/data:/app/data `
+ -v ${PWD}/checkpoints:/app/checkpoints `
+ -v ${PWD}/.env:/app/.env:ro `
+ --env-file .env `
+ sacdalance/thesis-aigvdet:gpu `
+ python train.py --exp_name my_experiment
+
+# CPU version
+docker run `
+ -v ${PWD}/data:/app/data `
+ -v ${PWD}/checkpoints:/app/checkpoints `
+ -v ${PWD}/.env:/app/.env:ro `
+ --env-file .env `
+ sacdalance/thesis-aigvdet:cpu `
+ python train.py --exp_name my_experiment
+```
+
+### 5. Verify Setup Before Training
+
+Run this to check if data is properly mounted:
+```bash
+docker run -v $(pwd)/data:/app/data sacdalance/thesis-aigvdet:gpu ls -la /app/data/train/trainset_1/
+```
+
+## Troubleshooting
+
+### Error: "No such file or directory: '/app/data/train/trainset_1'"
+
+**Solutions:**
+1. **Create the directory structure** (see step 1 above)
+2. **Verify volume mount** - ensure data directory exists locally
+3. **Check permissions** - ensure Docker can read the data directory
+
+### Error: "WANDB API key not found"
+
+**Solution:**
+- Create `.env` file with: `WANDB="your_api_key_here"`
+- Make sure it's mounted: `-v $(pwd)/.env:/app/.env:ro`
+- Or pass directly: `-e WANDB_API_KEY=your_api_key`
+
+### No GPU detected
+
+**Solutions:**
+1. Install nvidia-docker2:
+ ```bash
+ sudo apt-get install -y nvidia-docker2
+ sudo systemctl restart docker
+ ```
+2. Use `--gpus all` flag in docker run
+3. Or use CPU version instead
+
+### Permission Denied
+
+**Solution:**
+```bash
+# Fix data directory permissions
+chmod -R 755 data/
+chmod -R 755 checkpoints/
+```
+
+## Data Download Instructions
+
+See `DATA_SETUP.md` for complete instructions on downloading the training dataset from Baiduyun.
+
+Quick summary:
+1. Download from: https://pan.baidu.com/s/17xmDyFjtcmNsoxmUeImMTQ?pwd=ra95
+2. Extract to `data/train/trainset_1/`
+3. Ensure structure matches above format
+
+## Testing Without Full Dataset
+
+For quick testing, you can create a minimal dataset:
+1. Create a few sample frames (any images will do)
+2. Place in `data/train/trainset_1/0_real/video_00000/`
+3. Name them `00000.png`, `00001.png`, etc.
+4. Copy to `1_fake/` directory as well
+5. Run training to verify setup works
+
+## Training Options
+
+```bash
+# Basic training
+python train.py --exp_name my_experiment
+
+# With custom settings
+python train.py \
+ --exp_name my_experiment \
+ --batch_size 32 \
+ --nepoch 50 \
+ --lr 0.0001
+
+# Continue from checkpoint
+python train.py \
+ --exp_name my_experiment \
+ --continue_train \
+ --epoch 10
+```
diff --git a/md-files/WANDB_SETUP.md b/md-files/WANDB_SETUP.md
new file mode 100644
index 0000000..07dc9ad
--- /dev/null
+++ b/md-files/WANDB_SETUP.md
@@ -0,0 +1,216 @@
+# Weights & Biases (W&B) Integration Guide
+
+## What is W&B?
+Weights & Biases tracks your training runs, logs metrics, and **stores model checkpoints in the cloud** so you can access them from any device.
+
+## Setup Instructions
+
+### 1. Install wandb
+```bash
+# Local installation
+pip install wandb
+
+# Or with Docker (already included in requirements.txt)
+# Just rebuild the Docker image
+docker build -f Dockerfile.gpu-alt -t sacdalance/thesis-aigvdet:gpu .
+```
+
+### 2. Create W&B Account
+1. Go to https://wandb.ai/signup
+2. Sign up (free for personal use)
+3. Get your API key from https://wandb.ai/authorize
+
+### 3. Login to W&B
+
+**On local machine:**
+```bash
+wandb login
+# Paste your API key when prompted
+```
+
+**On Vast.ai or remote server:**
+```bash
+# Option 1: Interactive login
+wandb login
+
+# Option 2: Use API key directly
+export WANDB_API_KEY="your-api-key-here"
+# Or add to Docker run command:
+docker run --gpus all -e WANDB_API_KEY="your-key" ...
+```
+
+### 4. Train with W&B Tracking
+
+```bash
+# Train normally - W&B will automatically track
+python train.py --gpus 0 --exp_name TRAIN_RGB datasets trainset_1_RGB datasets_test val_set_1_RGB
+
+# With Docker on Vast.ai
+docker run --gpus all --shm-size=8g -it --rm \
+ -e WANDB_API_KEY="your-api-key" \
+ -v /workspace/data:/app/data \
+ -v /workspace/checkpoints:/app/checkpoints \
+ sacdalance/thesis-aigvdet:gpu \
+ python3.11 train.py --gpus 0 --exp_name TRAIN_RGB datasets trainset_1_RGB datasets_test val_set_1_RGB
+```
+
+## What W&B Tracks
+
+### Metrics Logged:
+- **Training loss** - Every batch
+- **Validation metrics** - Every epoch
+ - Accuracy (ACC)
+ - Average Precision (AP)
+ - AUC (Area Under Curve)
+ - TPR (True Positive Rate)
+ - TNR (True Negative Rate)
+- **System metrics** - GPU usage, CPU, memory
+
+### Model Checkpoints:
+- **Automatically uploads** `model_epoch_best.pth` to W&B cloud
+- **Access from anywhere** - Download checkpoints from any device
+- **Version control** - All checkpoint versions saved
+
+## Accessing Your Results
+
+### View Training Dashboard:
+1. Go to https://wandb.ai/
+2. Navigate to your project: `aigvdet-training`
+3. Click on your run (e.g., `TRAIN_RGB`)
+4. View real-time metrics, charts, and system stats
+
+### Download Checkpoints:
+```python
+# From any device with Python
+import wandb
+
+# Login
+wandb.login()
+
+# Download checkpoint
+api = wandb.Api()
+run = api.run("your-username/aigvdet-training/run-id")
+artifact = run.use_artifact('TRAIN_RGB_best_model:latest')
+artifact_dir = artifact.download()
+
+# The checkpoint will be in: artifact_dir/model_epoch_best.pth
+```
+
+**Or via Web UI:**
+1. Go to your run page
+2. Click "Artifacts" tab
+3. Click on `TRAIN_RGB_best_model`
+4. Click "Download" button
+
+## Benefits for Vast.ai Training
+
+### Problem W&B Solves:
+- ❌ **Without W&B**: Must manually download checkpoints before stopping Vast.ai instance
+- ✅ **With W&B**: Checkpoints automatically saved to cloud, accessible from anywhere
+
+### Workflow:
+```bash
+# 1. Start training on Vast.ai
+docker run --gpus all -e WANDB_API_KEY="your-key" ... python train.py ...
+
+# 2. Training runs (checkpoints auto-upload to W&B cloud)
+
+# 3. Stop Vast.ai instance (no need to manually download!)
+
+# 4. Later, on your local machine:
+wandb artifact get your-username/aigvdet-training/TRAIN_RGB_best_model:latest
+# Checkpoint downloaded to local machine!
+```
+
+## Configuration Options
+
+### Disable W&B (if needed):
+```bash
+# Set environment variable
+export WANDB_MODE=disabled
+
+# Or in code (add to train.py):
+os.environ["WANDB_MODE"] = "disabled"
+```
+
+### Change Project Name:
+Edit `train.py` line 43:
+```python
+wandb.init(
+ project="your-custom-project-name", # Change this
+ name=cfg.exp_name,
+ ...
+)
+```
+
+### Resume Training:
+W&B automatically handles resume if you use `continue_train True`:
+```bash
+python train.py --gpus 0 --exp_name TRAIN_RGB continue_train True epoch latest
+```
+
+## Cost
+- **Free tier**: Unlimited personal projects, 100GB storage
+- **Teams tier**: $50/user/month for collaboration
+- For academic research, you can apply for free team accounts
+
+## Troubleshooting
+
+### "wandb: ERROR api_key not configured"
+```bash
+# Set API key
+export WANDB_API_KEY="your-key"
+# Or login
+wandb login
+```
+
+### Checkpoint upload fails
+```bash
+# Check internet connection
+# Check W&B storage quota (free tier = 100GB)
+# Verify checkpoint file exists
+ls -lh data/exp/TRAIN_RGB/ckpt/model_epoch_best.pth
+```
+
+### Disable W&B temporarily
+```bash
+export WANDB_MODE=offline # Run offline, sync later
+# Or
+export WANDB_MODE=disabled # Completely disable
+```
+
+## Example Output
+
+When training starts:
+```
+Setting up TensorBoard...
+✓ Logs will be saved to: data/exp/TRAIN_RGB
+
+Initializing Weights & Biases...
+wandb: Currently logged in as: your-username (use `wandb login --relogin` to force relogin)
+wandb: Tracking run with wandb version 0.16.0
+wandb: Run data is saved locally in data/exp/TRAIN_RGB/wandb
+wandb: Run `wandb offline` to turn off syncing.
+wandb: Syncing run TRAIN_RGB
+wandb: ⭐️ View project at https://wandb.ai/your-username/aigvdet-training
+wandb: 🚀 View run at https://wandb.ai/your-username/aigvdet-training/runs/abc123
+✓ W&B tracking enabled: https://wandb.ai/your-username/aigvdet-training/runs/abc123
+```
+
+Click the URL to view your training in real-time!
+
+## Summary
+
+**With W&B integration:**
+1. ✅ Track training metrics in real-time from any device
+2. ✅ Automatically save checkpoints to cloud storage
+3. ✅ No need to manually download before stopping Vast.ai
+4. ✅ Compare multiple training runs easily
+5. ✅ Share results with collaborators
+
+**Setup is simple:**
+```bash
+pip install wandb
+wandb login
+python train.py # W&B automatically tracks!
+```
diff --git a/prepare_real_data.py b/prepare_real_data.py
new file mode 100644
index 0000000..51f89b7
--- /dev/null
+++ b/prepare_real_data.py
@@ -0,0 +1,175 @@
+import argparse
+import os
+import glob
+import shutil
+import cv2
+import numpy as np
+import torch
+import torch.nn
+from PIL import Image
+from tqdm import tqdm
+import sys
+
+# Add core to path to import RAFT
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+from natsort import natsorted
+
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+def load_image(imfile):
+ img = Image.open(imfile)
+
+ # Resize if too large (e.g. > 1024px) to prevent OOM
+ max_dim = 1024
+ if max(img.size) > max_dim:
+ scale = max_dim / max(img.size)
+ new_size = (int(img.size[0] * scale), int(img.size[1] * scale))
+ img = img.resize(new_size, Image.BILINEAR)
+
+ img = np.array(img).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def viz(img, flo, output_dir, filename):
+ img = img[0].permute(1,2,0).cpu().numpy()
+ flo = flo[0].permute(1,2,0).cpu().numpy()
+
+ # map flow to rgb image
+ flo = flow_viz.flow_to_image(flo)
+
+ # Save flow image
+ # The filename should match the input frame filename
+ save_path = os.path.join(output_dir, os.path.basename(filename))
+ cv2.imwrite(save_path, flo)
+
+def generate_optical_flow(model, frames_dir, output_dir):
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # Get all frames
+ images = sorted(glob.glob(os.path.join(frames_dir, '*.jpg')) +
+ glob.glob(os.path.join(frames_dir, '*.png')))
+ images = natsorted(images)
+
+ print(f"Generating optical flow for {len(images)} frames...")
+
+ with torch.no_grad():
+ for imfile1, imfile2 in tqdm(zip(images[:-1], images[1:]), total=len(images)-1, desc="Flow Generation"):
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+
+ viz(image1, flow_up, output_dir, imfile1)
+
+def main():
+ parser = argparse.ArgumentParser(description="Prepare Real Data for all datasets")
+ parser.add_argument("--source", type=str, required=True, help="Path to source folder containing multiple video folders (e.g. 1_real)")
+ parser.add_argument("--raft_model", type=str, default="raft_model/raft-things.pth", help="Path to RAFT model checkpoint")
+ parser.add_argument("--start_index", type=int, default=1, help="Index to start processing from (1-based)")
+ args = parser.parse_args()
+
+ if not os.path.exists(args.source):
+ print(f"❌ Source folder not found: {args.source}")
+ return
+
+ # Find all subdirectories (video folders)
+ video_folders = [f.path for f in os.scandir(args.source) if f.is_dir()]
+ video_folders = sorted(video_folders)
+
+ if not video_folders:
+ print(f"❌ No video folders found in {args.source}")
+ return
+
+ print(f"Found {len(video_folders)} video folders to process")
+
+ # Load RAFT model
+ print("Loading RAFT model...")
+
+ raft_args = argparse.Namespace(
+ model=args.raft_model,
+ small=False,
+ mixed_precision=False,
+ alternate_corr=False,
+ dropout=0
+ )
+
+ model = torch.nn.DataParallel(RAFT(raft_args))
+
+ try:
+ model.load_state_dict(torch.load(args.raft_model, map_location=torch.device(DEVICE)))
+ except FileNotFoundError:
+ print(f"❌ RAFT model not found at {args.raft_model}")
+ print("Please download it first using: python download_data.py --skip-data --skip-checkpoints")
+ return
+
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+ print("✓ RAFT model loaded")
+
+ # Target datasets
+ datasets = ["moonvalley", "videocraft", "pika", "neverends"]
+
+ # Process each video folder
+ for idx, video_path in enumerate(video_folders, 1):
+ if idx < args.start_index:
+ continue
+
+ video_name = os.path.basename(video_path)
+ print(f"\n{'='*60}")
+ print(f"Processing Video {idx}/{len(video_folders)}: {video_name}")
+ print(f"{'='*60}")
+
+ # 1. Generate Optical Flow ONCE in a temp location
+ temp_flow_dir = os.path.join("data", "temp_flow", video_name)
+ # Skip if already generated in temp
+ if os.path.exists(temp_flow_dir) and len(os.listdir(temp_flow_dir)) > 0:
+ print(f" Using existing flow in temp: {temp_flow_dir}")
+ else:
+ print(f" Generating optical flow to temp location: {temp_flow_dir}")
+ generate_optical_flow(model, video_path, temp_flow_dir)
+
+ # 2. Distribute to all datasets
+ print(f" Distributing to {len(datasets)} datasets...")
+
+ for dataset in datasets:
+ # Paths
+ rgb_dest = os.path.join("data", "test", "original", "T2V", dataset, "0_real", video_name)
+ flow_dest = os.path.join("data", "test", "T2V", dataset, "0_real", video_name)
+
+ # Create directories
+ os.makedirs(rgb_dest, exist_ok=True)
+ os.makedirs(flow_dest, exist_ok=True)
+
+ # Copy RGB frames
+ # print(f" -> RGB: {dataset}")
+ if os.path.exists(rgb_dest):
+ shutil.rmtree(rgb_dest)
+ shutil.copytree(video_path, rgb_dest)
+
+ # Copy Flow frames
+ # print(f" -> Flow: {dataset}")
+ if os.path.exists(flow_dest):
+ shutil.rmtree(flow_dest)
+ shutil.copytree(temp_flow_dir, flow_dest)
+
+ print("\n" + "="*80)
+ print("✓ REAL DATA PREPARATION COMPLETE")
+ print("="*80)
+ print(f"Source: {args.source}")
+ print(f"Processed {len(video_folders)} videos")
+ print(f"Distributed to: {', '.join(datasets)}")
+
+ # Clean up temp
+ if os.path.exists(os.path.join("data", "temp_flow")):
+ shutil.rmtree(os.path.join("data", "temp_flow"))
+
+if __name__ == "__main__":
+ main()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..93e2090
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,62 @@
+[project]
+name = "aigvdet"
+version = "0.1.0"
+description = "AI-Generated Video Detection via Spatial-Temporal Anomaly Learning"
+authors = [
+ {name = "Jianfa Bai"},
+ {name = "Man Lin"},
+ {name = "Gang Cao"},
+ {name = "Zijie Lou"}
+]
+readme = "README.md"
+requires-python = ">=3.11, <3.12"
+dependencies = [
+ "torch==2.0.0+cu117",
+ "torchvision==0.15.1+cu117",
+ "einops",
+ "imageio",
+ "ipympl",
+ "matplotlib",
+ "natsort",
+ "numpy<2.0",
+ "opencv-python",
+ "pandas",
+ "scikit-learn",
+ "tensorboard",
+ "tensorboardX",
+ "tqdm",
+ "blobfile>=1.0.5",
+ "wandb",
+ "python-dotenv",
+ "pip",
+]
+
+[project.urls]
+Repository = "https://github.com/sacdalance/AIGVDet"
+
+[project.scripts]
+aigvdet-train = "train:main"
+aigvdet-test = "test:main"
+aigvdet-demo = "demo:main"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["core"]
+
+[tool.uv.sources]
+torch = { index = "pytorch" }
+torchvision = { index = "pytorch" }
+
+[[tool.uv.index]]
+name = "pytorch"
+url = "https://download.pytorch.org/whl/cu117"
+
+[dependency-groups]
+dev = [
+ "hatchling",
+ "setuptools",
+ "wheel",
+]
diff --git a/python-utils/download_t2v.py b/python-utils/download_t2v.py
new file mode 100644
index 0000000..19a20f9
--- /dev/null
+++ b/python-utils/download_t2v.py
@@ -0,0 +1,31 @@
+import gdown
+import zipfile
+import os
+
+# Google Drive file ID
+file_id = "1FT06IRiy1oB1jHWBEarUI99DFCk6VHxf"
+
+# Output path for the downloaded file
+output_path = "downloaded_file.zip"
+
+# Download the file from Google Drive
+gdown.download(f"https://drive.google.com/uc?id={file_id}", output_path, quiet=False)
+
+# Check if the file was downloaded
+if os.path.exists(output_path):
+ print(f"File downloaded successfully: {output_path}")
+
+ # Unzip the file
+ unzip_dir = "unzipped_files"
+ if not os.path.exists(unzip_dir):
+ os.makedirs(unzip_dir)
+
+ with zipfile.ZipFile(output_path, 'r') as zip_ref:
+ zip_ref.extractall(unzip_dir)
+ print(f"Files extracted to: {unzip_dir}")
+
+ # Optionally, you can delete the zip file after extraction
+ os.remove(output_path)
+ print("Zip file removed after extraction.")
+else:
+ print("Download failed.")
diff --git a/python-utils/prepare_data.py b/python-utils/prepare_data.py
new file mode 100644
index 0000000..b72bb1a
--- /dev/null
+++ b/python-utils/prepare_data.py
@@ -0,0 +1,159 @@
+import sys
+import argparse
+import os
+import cv2
+import glob
+import numpy as np
+import torch
+from PIL import Image
+from tqdm import tqdm
+from natsort import natsorted
+
+# Add core to path for imports
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+def load_image(imfile):
+ """
+ Load an image, convert to tensor, and move to the appropriate device.
+ """
+ img = np.array(Image.open(imfile)).astype(np.uint8)
+
+ return img[None].to(DEVICE)
+
+def save_flow(img, flo, output_path):
+ """
+ Save optical flow as a color image.
+ """
+ img = img[0].permute(1, 2, 0).cpu().numpy()
+ flo = flo[0].permute(1, 2, 0).cpu().numpy()
+
+ # Convert flow to RGB
+ flo = flow_viz.flow_to_image(flo)
+ cv2.imwrite(output_path, flo)
+
+def video_to_frames(video_path, output_folder, max_frames=95):
+ """
+ Extract frames from a video and save them as images in the output folder.
+ Limits to max_frames (default 95) to match paper methodology.
+ """
+ if not os.path.exists(output_folder):
+ os.makedirs(output_folder)
+
+ # Check if frames already exist to skip
+ existing_frames = glob.glob(os.path.join(output_folder, "*.png"))
+ if len(existing_frames) > 0:
+ return sorted(existing_frames)[:max_frames]
+
+ cap = cv2.VideoCapture(video_path)
+ frame_count = 0
+
+ while cap.isOpened() and frame_count < max_frames:
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ frame_filename = os.path.join(output_folder, f"frame_{frame_count:05d}.png")
+ cv2.imwrite(frame_filename, frame)
+ frame_count += 1
+
+ cap.release()
+
+ # Return sorted list of extracted frame files
+ images = glob.glob(os.path.join(output_folder, '*.png')) + \
+ glob.glob(os.path.join(output_folder, '*.jpg'))
+ return sorted(images)[:max_frames]
+
+def process_dataset(args):
+ """
+ Process each video in the dataset, extracting frames and computing optical flow.
+ """
+ # Load RAFT model once
+ print(f"Loading RAFT model from {args.model}...")
+ model = torch.nn.DataParallel(RAFT(args))
+ model.load_state_dict(torch.load(args.model, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+ print("✓ RAFT model loaded")
+
+ # Structure: source_dir / [0_real, 1_fake] / video.mp4
+ for label in ["0_real", "1_fake"]:
+ source_label_dir = os.path.join(args.source_dir, label)
+ if not os.path.exists(source_label_dir):
+ print(f"Skipping {label}, directory not found: {source_label_dir}")
+ continue
+
+ videos = glob.glob(os.path.join(source_label_dir, "*.mp4")) + \
+ glob.glob(os.path.join(source_label_dir, "*.avi")) + \
+ glob.glob(os.path.join(source_label_dir, "*.mov"))
+
+ print(f"Found {len(videos)} videos in {label}")
+
+ for video_path in tqdm(videos, desc=f"Processing {label}"):
+ video_name = os.path.splitext(os.path.basename(video_path))[0]
+
+ # Define output paths for RGB frames and optical flow
+ rgb_out_dir = os.path.join(args.output_rgb_dir, label, video_name)
+ flow_out_dir = os.path.join(args.output_flow_dir, label, video_name)
+
+ if not os.path.exists(flow_out_dir):
+ os.makedirs(flow_out_dir)
+
+ # 1. Extract Frames
+ images = video_to_frames(video_path, rgb_out_dir)
+ images = natsorted(images)
+
+ if len(images) < 2:
+ continue
+
+ # 2. Generate Optical Flow
+ # Check if flow already exists for the extracted frames
+ existing_flow = glob.glob(os.path.join(flow_out_dir, "*.png"))
+ if len(existing_flow) >= len(images) - 1:
+ continue
+
+ with torch.no_grad():
+ for i, (imfile1, imfile2) in enumerate(zip(images[:-1], images[1:])):
+ # Output filename matches input filename
+ flow_filename = os.path.basename(imfile1)
+ flow_output_path = os.path.join(flow_out_dir, flow_filename)
+
+ if os.path.exists(flow_output_path):
+ continue
+
+ # Load the consecutive frames
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ # Pad images if necessary
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ # Compute flow with the RAFT model
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+
+ # Save the optical flow
+ save_flow(image1, flow_up, flow_output_path)
+
+if __name__ == '__main__':
+ # Parse command-line arguments
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--source_dir', required=True, help="Path to folder containing 0_real/1_fake video folders")
+ parser.add_argument('--output_rgb_dir', required=True, help="Output path for RGB frames")
+ parser.add_argument('--output_flow_dir', required=True, help="Output path for Optical Flow frames")
+ parser.add_argument('--model', default="raft_model/raft-things.pth", help="Path to RAFT model checkpoint")
+
+ # RAFT arguments
+ parser.add_argument('--small', action='store_true', help='Use small model')
+ parser.add_argument('--mixed_precision', action='store_true', help='Use mixed precision')
+ parser.add_argument('--alternate_corr', action='store_true', help='Use efficient correlation implementation')
+
+ args = parser.parse_args()
+
+ # Process the dataset with the provided arguments
+ process_dataset(args)
diff --git a/python-utils/process_custom_videos.py b/python-utils/process_custom_videos.py
new file mode 100644
index 0000000..c759f24
--- /dev/null
+++ b/python-utils/process_custom_videos.py
@@ -0,0 +1,184 @@
+import sys
+import argparse
+import os
+import cv2
+import glob
+import numpy as np
+import torch
+from PIL import Image
+from tqdm import tqdm
+from natsort import natsorted
+import warnings
+warnings.filterwarnings("ignore", category=UserWarning, message="torch.meshgrid")
+
+# Add core to path for imports
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+
+if not torch.cuda.is_available():
+ raise RuntimeError("CUDA is not available. This script requires a GPU.")
+DEVICE = 'cuda'
+
+def load_image(imfile):
+ img = np.array(Image.open(imfile)).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def save_flow(img, flo, output_path):
+ img = img[0].permute(1, 2, 0).cpu().numpy()
+ flo = flo[0].permute(1, 2, 0).cpu().numpy()
+
+ # map flow to rgb image
+ flo = flow_viz.flow_to_image(flo)
+ # Save only the flow image as per typical requirements, or concatenated if requested.
+ # The user's code had: img_flo = np.concatenate([img, flo], axis=0)
+ # But usually for training we just want the flow.
+ # The user's demo code saved 'flo'.
+ cv2.imwrite(output_path, flo)
+
+def video_to_frames(video_path, output_folder, max_frames=95, resize=None):
+ if not os.path.exists(output_folder):
+ os.makedirs(output_folder)
+
+ cap = cv2.VideoCapture(video_path)
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
+
+ # Print resolution to help user understand speed
+ print(f" -> Video: {os.path.basename(video_path)} | Resolution: {width}x{height}")
+
+ frame_count = 0
+ saved_frames = []
+
+ while cap.isOpened() and frame_count < max_frames:
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ # Resize if requested
+ if resize is not None:
+ h, w = frame.shape[:2]
+ if min(h, w) > resize:
+ scale = resize / min(h, w)
+ new_h, new_w = int(h * scale), int(w * scale)
+ frame = cv2.resize(frame, (new_w, new_h))
+
+ video_name = os.path.splitext(os.path.basename(video_path))[0]
+ frame_filename = os.path.join(output_folder, f"{video_name}_{frame_count+1:05d}.jpg")
+ cv2.imwrite(frame_filename, frame)
+ saved_frames.append(frame_filename)
+ frame_count += 1
+
+ cap.release()
+ return sorted(saved_frames)
+
+def process_videos(args):
+ # Verify model path
+ if not os.path.exists(args.model):
+ # Try fallback for common issue (hyphen vs underscore)
+ if "raft-model" in args.model and os.path.exists(args.model.replace("raft-model", "raft_model")):
+ args.model = args.model.replace("raft-model", "raft_model")
+ print(f"Found model at alternate path: {args.model}")
+ elif "raft_model" in args.model and os.path.exists(args.model.replace("raft_model", "raft-model")):
+ args.model = args.model.replace("raft_model", "raft-model")
+ print(f"Found model at alternate path: {args.model}")
+ else:
+ print(f"Error: Model file not found at {args.model}")
+ print(f"Current working directory: {os.getcwd()}")
+ print(f"Available directories: {[d for d in os.listdir('.') if os.path.isdir(d)]}")
+ raise FileNotFoundError(f"Model file not found: {args.model}")
+
+ # Load model
+ model = torch.nn.DataParallel(RAFT(args))
+ model.load_state_dict(torch.load(args.model, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+
+ print(f"Processing on device: {DEVICE}")
+ if args.resize:
+ print(f"Resizing frames to short edge: {args.resize}px")
+
+ # Get list of videos
+ videos = glob.glob(os.path.join(args.input_path, '*.mp4')) + \
+ glob.glob(os.path.join(args.input_path, '*.avi')) + \
+ glob.glob(os.path.join(args.input_path, '*.mov'))
+
+ print(f"Found {len(videos)} videos in {args.input_path}")
+
+ for video_path in tqdm(videos):
+ video_name = os.path.splitext(os.path.basename(video_path))[0]
+
+ # Use main output directories directly (flat structure)
+ video_rgb_dir = args.output_rgb
+ video_flow_dir = args.output_flow
+
+ if not os.path.exists(video_rgb_dir):
+ os.makedirs(video_rgb_dir)
+ if not os.path.exists(video_flow_dir):
+ os.makedirs(video_flow_dir)
+
+ # 1. Extract RGB frames (max 95)
+ # Pass resize argument here
+ images = video_to_frames(video_path, video_rgb_dir, max_frames=95, resize=args.resize)
+
+ # 2. Compute Optical Flow (max 94)
+ if len(images) < 2:
+ continue
+
+ with torch.no_grad():
+ images = natsorted(images)
+
+ # Load first image
+ image1 = load_image(images[0])
+
+ for i in range(len(images) - 1):
+ if i >= 94:
+ break
+
+ imfile1 = images[i]
+ imfile2 = images[i+1]
+
+ # Check if flow already exists
+ flow_filename = os.path.basename(imfile1)
+ flow_output_path = os.path.join(video_flow_dir, flow_filename)
+
+ if os.path.exists(flow_output_path):
+ # If skipping, we still need to update image1 for the next iteration if we weren't reloading
+ # But since we are skipping, we might not have loaded image2 yet.
+ # To be safe and simple with skipping logic:
+ image1 = load_image(imfile2) # Prepare for next iter
+ continue
+
+ image2 = load_image(imfile2)
+
+ # Debug: Print shape once to confirm resize
+ if i == 0 and video_path == videos[0]:
+ print(f" -> Model input shape: {image1.shape}")
+
+ padder = InputPadder(image1.shape)
+ image1_padded, image2_padded = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1_padded, image2_padded, iters=20, test_mode=True)
+
+ save_flow(image1, flow_up, flow_output_path)
+
+ # Move image2 to image1 for next iteration
+ image1 = image2
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--model', help="restore checkpoint", default="raft-model/raft-things.pth")
+ parser.add_argument('--input_path', help="path to videos", default="data/T2V/moonvalley_mp4")
+ parser.add_argument('--output_rgb', help="output path for RGB frames", default="data/T2V/moonvalley_rgb")
+ parser.add_argument('--output_flow', help="output path for Optical Flow frames", default="data/T2V/moonvalley_flow")
+ parser.add_argument('--small', action='store_true', help='use small model')
+ parser.add_argument('--mixed_precision', action='store_true', help='use mixed precision')
+ parser.add_argument('--alternate_corr', action='store_true', help='use efficent correlation implementation')
+ parser.add_argument('--resize', type=int, default=None, help='Resize smaller edge of frames to this value (e.g. 512) for faster processing')
+
+ args = parser.parse_args()
+
+ process_videos(args)
diff --git a/python-utils/recreate_table_2.py b/python-utils/recreate_table_2.py
new file mode 100644
index 0000000..2674edb
--- /dev/null
+++ b/python-utils/recreate_table_2.py
@@ -0,0 +1,82 @@
+import argparse
+import subprocess
+import os
+import sys
+
+def run_command(command):
+ print(f"Running: {command}")
+ try:
+ subprocess.check_call(command, shell=True)
+ except subprocess.CalledProcessError as e:
+ print(f"Error running command: {command}")
+ sys.exit(1)
+
+def main():
+ parser = argparse.ArgumentParser(description="Recreate Table 2 results for a specific dataset.")
+ parser.add_argument("dataset", help="Name of the dataset (e.g., moonvalley, videocraft, pika, neverends)")
+ parser.add_argument("--rgb_dir", help="Path to RGB frames directory", default=None)
+ parser.add_argument("--flow_dir", help="Path to Optical Flow frames directory", default=None)
+ args = parser.parse_args()
+
+ dataset_name = args.dataset
+
+ # Define paths
+ # Default paths (relative to current directory)
+ base_data_dir = "data"
+
+ # If arguments are provided, use them. Otherwise, construct default paths.
+ if args.rgb_dir:
+ output_rgb_dir = args.rgb_dir
+ else:
+ # Matches structure: data/test/videocraft_rgb
+ output_rgb_dir = os.path.join(base_data_dir, "test", f"{dataset_name}_rgb")
+
+ if args.flow_dir:
+ output_flow_dir = args.flow_dir
+ else:
+ # Matches structure: data/test/videocraft_flow
+ output_flow_dir = os.path.join(base_data_dir, "test", f"{dataset_name}_flow")
+
+ # Source video dir (only needed for preparation step, which is skipped)
+ source_video_dir = os.path.join(base_data_dir, "test", "T2V", dataset_name)
+
+ result_csv = os.path.join(base_data_dir, "results", f"{dataset_name}.csv")
+ result_no_cp_csv = os.path.join(base_data_dir, "results", f"{dataset_name}_no_cp.csv")
+
+ # Model paths (relative)
+ raft_model = "raft_model/raft-things.pth"
+ optical_model = "checkpoints/optical.pth"
+ original_model = "checkpoints/original.pth"
+
+ print(f"--- Processing Dataset: {dataset_name} ---")
+ print(f"RGB Directory: {output_rgb_dir}")
+ print(f"Flow Directory: {output_flow_dir}")
+
+ # Check if directories exist
+ if not os.path.exists(output_rgb_dir):
+ print(f"Warning: RGB directory not found: {output_rgb_dir}")
+ if not os.path.exists(output_flow_dir):
+ print(f"Warning: Flow directory not found: {output_flow_dir}")
+
+ # Step 1: Prepare Data
+ # print("\n[Step 1] Preparing Data (Extracting Frames & Generating Optical Flow)...")
+ # # Note: We use sys.executable to ensure we use the same python interpreter
+ # cmd_prepare = f'"{sys.executable}" prepare_data.py --source_dir "{source_video_dir}" --output_rgb_dir "{output_rgb_dir}" --output_flow_dir "{output_flow_dir}" --model "{raft_model}"'
+ # run_command(cmd_prepare)
+
+ # Step 2: Run Standard Evaluation
+ print("\n[Step 2] Running Standard Evaluation (AIGVDet, Sspatial, Soptical)...")
+ cmd_test = f'"{sys.executable}" test_flat.py -fop "{output_flow_dir}" -for "{output_rgb_dir}" -mop "{optical_model}" -mor "{original_model}" -e "{result_csv}"'
+ run_command(cmd_test)
+
+ # Step 3: Run No-Crop Evaluation
+ print("\n[Step 3] Running No-Crop Evaluation (Soptical no cp)...")
+ cmd_test_no_cp = f'"{sys.executable}" test_flat.py --no_crop -fop "{output_flow_dir}" -for "{output_rgb_dir}" -mop "{optical_model}" -mor "{original_model}" -e "{result_no_cp_csv}"'
+ run_command(cmd_test_no_cp)
+
+ print("\n--- Done! ---")
+ print(f"Standard Results saved to: {result_csv}")
+ print(f"No-Crop Results saved to: {result_no_cp_csv}")
+
+if __name__ == "__main__":
+ main()
diff --git a/python-utils/test_flat.py b/python-utils/test_flat.py
new file mode 100644
index 0000000..cc2668b
--- /dev/null
+++ b/python-utils/test_flat.py
@@ -0,0 +1,159 @@
+import argparse
+import glob
+import os
+import pandas as pd
+import torch
+import torch.nn
+import torchvision.transforms as transforms
+import torchvision.transforms.functional as TF
+from PIL import Image
+from tqdm import tqdm
+from sklearn.metrics import accuracy_score, roc_auc_score
+from core.utils1.utils import get_network, str2bool
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-fop", "--folder_optical_flow_path", type=str, required=True)
+ parser.add_argument("-for", "--folder_original_path", type=str, required=True)
+ parser.add_argument("-mop", "--model_optical_flow_path", type=str, default="checkpoints/optical.pth")
+ parser.add_argument("-mor", "--model_original_path", type=str, default="checkpoints/original.pth")
+ parser.add_argument("-t", "--threshold", type=float, default=0.5)
+ parser.add_argument("-e", "--excel_path", type=str, default="results.csv")
+ parser.add_argument("--use_cpu", action="store_true")
+ parser.add_argument("--arch", type=str, default="resnet50")
+ parser.add_argument("--aug_norm", type=str2bool, default=True)
+ parser.add_argument("--no_crop", action="store_true")
+
+ args = parser.parse_args()
+
+ # Load Models
+ device = torch.device("cpu" if args.use_cpu else "cuda")
+
+ print("Loading models...")
+ model_op = get_network(args.arch).to(device)
+ model_op.load_state_dict(torch.load(args.model_optical_flow_path, map_location=device)["model"])
+ model_op.eval()
+
+ model_or = get_network(args.arch).to(device)
+ model_or.load_state_dict(torch.load(args.model_original_path, map_location=device)["model"])
+ model_or.eval()
+
+ # Transforms
+ if args.no_crop:
+ trans = transforms.Compose([transforms.ToTensor()])
+ else:
+ trans = transforms.Compose([transforms.CenterCrop((448, 448)), transforms.ToTensor()])
+
+ print(f"Processing flat directories...")
+ print(f"RGB: {args.folder_original_path}")
+ print(f"Flow: {args.folder_optical_flow_path}")
+
+ # Get list of images
+ # We assume filenames match between RGB and Flow (except maybe extension)
+ # Actually, process_custom_videos outputs:
+ # RGB: video_name_frame_00001.jpg
+ # Flow: video_name_frame_00001.jpg (or .png)
+
+ rgb_files = sorted(glob.glob(os.path.join(args.folder_original_path, "*")))
+ rgb_files = [f for f in rgb_files if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
+
+ if len(rgb_files) == 0:
+ print("No RGB images found!")
+ return
+
+ print(f"Found {len(rgb_files)} RGB frames.")
+
+ y_true = []
+ y_pred = []
+ y_pred_rgb = []
+ y_pred_flow = []
+
+ results = []
+
+ # Iterate
+ for rgb_path in tqdm(rgb_files):
+ filename = os.path.basename(rgb_path)
+ # Try to find corresponding flow file
+ # Flow might be .png even if RGB is .jpg
+ flow_path = os.path.join(args.folder_optical_flow_path, filename)
+ if not os.path.exists(flow_path):
+ # Try replacing extension
+ name, ext = os.path.splitext(filename)
+ flow_path_png = os.path.join(args.folder_optical_flow_path, name + ".png")
+ flow_path_jpg = os.path.join(args.folder_optical_flow_path, name + ".jpg")
+ if os.path.exists(flow_path_png):
+ flow_path = flow_path_png
+ elif os.path.exists(flow_path_jpg):
+ flow_path = flow_path_jpg
+ else:
+ # print(f"Warning: Flow file not found for {filename}")
+ continue
+
+ # Load and Preprocess
+ try:
+ img_rgb = Image.open(rgb_path).convert("RGB")
+ img_flow = Image.open(flow_path).convert("RGB")
+ except Exception as e:
+ print(f"Error loading {filename}: {e}")
+ continue
+
+ # Transform
+ t_rgb = trans(img_rgb)
+ t_flow = trans(img_flow)
+
+ if args.aug_norm:
+ t_rgb = TF.normalize(t_rgb, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ t_flow = TF.normalize(t_flow, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+
+ t_rgb = t_rgb.unsqueeze(0).to(device)
+ t_flow = t_flow.unsqueeze(0).to(device)
+
+ # Inference
+ with torch.no_grad():
+ prob_rgb = model_or(t_rgb).sigmoid().item()
+ prob_flow = model_op(t_flow).sigmoid().item()
+
+ prob_fused = 0.5 * prob_rgb + 0.5 * prob_flow
+
+ # Assume Fake (1) since we are testing generators
+ label = 1
+
+ y_true.append(label)
+ y_pred.append(prob_fused)
+ y_pred_rgb.append(prob_rgb)
+ y_pred_flow.append(prob_flow)
+
+ results.append({
+ "filename": filename,
+ "prob_fused": prob_fused,
+ "prob_rgb": prob_rgb,
+ "prob_flow": prob_flow,
+ "label": label
+ })
+
+ # Metrics
+ if len(y_true) == 0:
+ print("No valid pairs processed.")
+ return
+
+ acc_fused = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred])
+ acc_rgb = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred_rgb])
+ acc_flow = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred_flow])
+
+ print("-" * 30)
+ print(f"Results (Assuming all inputs are FAKE/Generated)")
+ print(f"Total Frames: {len(y_true)}")
+ print("-" * 30)
+ print(f"Fused Accuracy (Recall): {acc_fused:.4f}")
+ print(f"RGB Accuracy (Recall): {acc_rgb:.4f}")
+ print(f"Flow Accuracy (Recall): {acc_flow:.4f}")
+ print("-" * 30)
+
+ # Save CSV
+ df = pd.DataFrame(results)
+ os.makedirs(os.path.dirname(args.excel_path), exist_ok=True)
+ df.to_csv(args.excel_path, index=False)
+ print(f"Saved results to {args.excel_path}")
+
+if __name__ == "__main__":
+ main()
diff --git a/recreate_table2_data2.py b/recreate_table2_data2.py
new file mode 100644
index 0000000..819c80e
--- /dev/null
+++ b/recreate_table2_data2.py
@@ -0,0 +1,309 @@
+"""
+Script to recreate Table 2 for data2 (Emu, Hotshot, Sora)
+Evaluates only the 3 main variants: AIGVDet (fused), Spatial, Optical
+"""
+
+import os
+import subprocess
+import pandas as pd
+from pathlib import Path
+import re
+import argparse
+
+# Configuration for data2 datasets
+DATASETS = {
+ "emu": {
+ "optical": "data2/test/T2V/emu",
+ "rgb": "data2/test/original/T2V/emu"
+ },
+ "hotshot": {
+ "optical": "data2/test/T2V/hotshot",
+ "rgb": "data2/test/original/T2V/hotshot"
+ },
+ "sora": {
+ "optical": "data2/test/T2V/sora",
+ "rgb": "data2/test/original/T2V/sora"
+ }
+}
+
+# Model configurations - Only 3 variants for Table 2
+VARIANTS = {
+ "S_spatial": {
+ "eval_mode": "rgb_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "S_optical": {
+ "eval_mode": "optical_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "AIGVDet": {
+ "eval_mode": "fused",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ }
+}
+
+RESULTS_DIR = Path("data2/results/table2")
+RESULTS_DIR.mkdir(parents=True, exist_ok=True)
+
+def check_prerequisites():
+ """Check if all required datasets and models exist"""
+ print("="*80)
+ print("CHECKING PREREQUISITES FOR DATA2")
+ print("="*80)
+
+ # Check datasets
+ print("\n1. Checking datasets...")
+ missing_data = []
+ for dataset_name, paths in DATASETS.items():
+ for stream_type, path in paths.items():
+ if not os.path.exists(path):
+ missing_data.append(f" ❌ {dataset_name} ({stream_type}): {path}")
+ else:
+ real_path = os.path.join(path, "0_real")
+ fake_path = os.path.join(path, "1_fake")
+
+ status = []
+ if os.path.exists(real_path):
+ status.append("Real ✓")
+ else:
+ status.append("Real ✗")
+
+ if os.path.exists(fake_path):
+ status.append("Fake ✓")
+ else:
+ status.append("Fake ✗")
+
+ print(f" ✓ {dataset_name} ({stream_type}): {path} [{', '.join(status)}]")
+
+ if not os.path.exists(real_path) and not os.path.exists(fake_path):
+ missing_data.append(f" ⚠️ {dataset_name} ({stream_type}): Missing BOTH 0_real and 1_fake folders")
+
+ if missing_data:
+ print("\n⚠️ Some data is missing:")
+ for item in missing_data:
+ print(item)
+
+ # Check models
+ print("\n2. Checking model checkpoints...")
+ models = ["checkpoints/optical.pth", "checkpoints/original.pth"]
+ missing_models = []
+ for model in models:
+ if os.path.exists(model):
+ print(f" ✓ {model}")
+ else:
+ print(f" ❌ {model}")
+ missing_models.append(model)
+
+ if missing_models:
+ print("\n❌ Missing models. Please ensure checkpoints are available.")
+ return False
+
+ if missing_data:
+ print("\n⚠️ Some datasets are incomplete but will continue...")
+
+ return True
+
+def run_evaluation(dataset_name, variant_name, variant_config, limit=None):
+ """Run evaluation for a specific dataset and variant"""
+ dataset_paths = DATASETS[dataset_name]
+
+ # Build command
+ cmd = [
+ "python", "test_single_stream.py",
+ "-fop", dataset_paths["optical"],
+ "-for", dataset_paths["rgb"],
+ "-mop", variant_config["optical_model"],
+ "-mor", variant_config["rgb_model"],
+ "--eval_mode", variant_config["eval_mode"],
+ "-e", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"),
+ "-ef", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_frame.csv"),
+ "-t", "0.5"
+ ]
+
+ if variant_config["no_crop"]:
+ cmd.append("--no_crop")
+
+ if limit:
+ cmd.extend(["--limit", str(limit)])
+
+ print(f"\n{'='*80}")
+ print(f"Running: {variant_name} on {dataset_name}")
+ print(f"{'='*80}")
+ print("Command:", " ".join(cmd))
+ print(f"{'='*80}\n")
+
+ # Run the command with real-time output
+ try:
+ # Use Popen to stream output in real-time
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ # Collect output while displaying it
+ output_lines = []
+ for line in process.stdout:
+ print(line, end='', flush=True) # Print in real-time
+ output_lines.append(line)
+
+ # Wait for process to complete
+ return_code = process.wait(timeout=3600)
+ output = ''.join(output_lines)
+
+ if return_code != 0:
+ print(f"\n⚠️ Command exited with code {return_code}")
+
+ # Parse metrics from CSV file
+ csv_path = RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"
+ metrics = parse_metrics(output, csv_path)
+ return metrics
+ except subprocess.TimeoutExpired:
+ print(f"\n⚠️ Timeout while running {variant_name} on {dataset_name}")
+ process.kill()
+ return None
+ except Exception as e:
+ print(f"\n❌ Error running {variant_name} on {dataset_name}: {e}")
+ return None
+
+def parse_metrics(output, csv_path):
+ """Parse metrics from CSV file (test_single_stream.py doesn't print to stdout)"""
+ try:
+ # Read the CSV file
+ if not os.path.exists(csv_path):
+ print(f" ⚠️ CSV file not found: {csv_path}")
+ return None
+
+ df = pd.read_csv(csv_path)
+
+ if len(df) == 0:
+ print(f" ⚠️ CSV file is empty: {csv_path}")
+ return None
+
+ # Calculate metrics from the CSV data
+ from sklearn.metrics import accuracy_score, roc_auc_score, average_precision_score
+
+ y_true = df['flag'].values # Ground truth (0=real, 1=fake)
+ y_pred = df['pro'].values # Predicted probability
+
+ # Calculate metrics
+ acc = accuracy_score(y_true, (y_pred >= 0.5).astype(int))
+ auc = roc_auc_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+ ap = average_precision_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+
+ metrics = {
+ 'ACC': acc,
+ 'AUC': auc,
+ 'AP': ap
+ }
+
+ print(f"\n ✓ Metrics: ACC={acc:.4f}, AUC={auc:.4f}, AP={ap:.4f}")
+ return metrics
+
+ except Exception as e:
+ print(f" ⚠️ Error reading metrics from CSV: {e}")
+ return None
+
+def run_all_evaluations(limit=None):
+ """Run evaluations for all variants and datasets"""
+ print("\n" + "="*80)
+ print("RUNNING EVALUATIONS FOR DATA2")
+ print("="*80)
+
+ all_results = {}
+
+ total_evals = len(VARIANTS) * len(DATASETS)
+ current_eval = 0
+
+ for variant_idx, (variant_name, variant_config) in enumerate(VARIANTS.items(), 1):
+ all_results[variant_name] = {}
+
+ print(f"\n{'='*80}")
+ print(f"VARIANT {variant_idx}/{len(VARIANTS)}: {variant_name}")
+ print(f"{'='*80}")
+
+ for dataset_idx, dataset_name in enumerate(DATASETS.keys(), 1):
+ current_eval += 1
+ overall_progress = (current_eval / total_evals) * 100
+
+ print(f"\n[Overall: {current_eval}/{total_evals} - {overall_progress:.1f}%]")
+ print(f"[Variant: {variant_idx}/{len(VARIANTS)}] [{variant_name}]")
+ print(f"[Dataset: {dataset_idx}/{len(DATASETS)}] [{dataset_name}]")
+
+ metrics = run_evaluation(dataset_name, variant_name, variant_config, limit)
+ all_results[variant_name][dataset_name] = metrics
+
+ return all_results
+
+def compile_table2(results):
+ """Compile results into Table 2 format"""
+ print("\n" + "="*80)
+ print("COMPILING TABLE 2 (DATA2)")
+ print("="*80)
+
+ # Create DataFrame
+ rows = []
+ for dataset in DATASETS.keys():
+ row = {"Dataset": dataset}
+ for variant in VARIANTS.keys():
+ if results[variant][dataset]:
+ auc = results[variant][dataset].get('AUC', 0) * 100
+ ap = results[variant][dataset].get('AP', 0) * 100
+ row[variant] = f"{auc:.1f}/{ap:.1f}"
+ else:
+ row[variant] = "N/A"
+ rows.append(row)
+
+ df = pd.DataFrame(rows)
+
+ # Save to CSV
+ output_file = RESULTS_DIR / "table2_data2.csv"
+ df.to_csv(output_file, index=False)
+
+ print(f"\n✓ Table 2 saved to: {output_file}")
+ print("\nTable 2 Preview:")
+ print(df.to_string(index=False))
+
+ return df
+
+def main():
+ """Main execution function"""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--limit", type=int, default=None, help="Limit number of videos per class")
+ args = parser.parse_args()
+
+ print("="*80)
+ print("RECREATING TABLE 2 FOR DATA2 (EMU, HOTSHOT, SORA)")
+ print("="*80)
+ print("\nThis script will:")
+ print("1. Check prerequisites (data and models)")
+ print("2. Run evaluations for 3 variants (AIGVDet, Spatial, Optical)")
+ print("3. Compile results into Table 2 format")
+ print("="*80)
+
+ # Check prerequisites
+ if not check_prerequisites():
+ print("\n❌ Prerequisites not satisfied. Please fix the issues above.")
+ return
+
+ # Run all evaluations
+ all_results = run_all_evaluations(limit=args.limit)
+
+ # Compile and display Table 2
+ table = compile_table2(all_results)
+
+ print("\n" + "="*80)
+ print("✅ TABLE 2 RECREATION COMPLETE!")
+ print("="*80)
+
+if __name__ == "__main__":
+ main()
diff --git a/recreate_table2_data3.py b/recreate_table2_data3.py
new file mode 100644
index 0000000..96fc079
--- /dev/null
+++ b/recreate_table2_data3.py
@@ -0,0 +1,309 @@
+"""
+Script to recreate Table 2 for data3 (I2V: Moonvalley, Pika, NeverEnds)
+Evaluates only the 3 main variants: AIGVDet (fused), Spatial, Optical
+"""
+
+import os
+import subprocess
+import pandas as pd
+from pathlib import Path
+import re
+import argparse
+
+# Configuration for data3 I2V datasets
+DATASETS = {
+ "moonvalley": {
+ "optical": "data3/test/I2V/moonvalley",
+ "rgb": "data3/test/original/I2V/moonvalley"
+ },
+ "pika": {
+ "optical": "data3/test/I2V/pika",
+ "rgb": "data3/test/original/I2V/pika"
+ },
+ "neverends": {
+ "optical": "data3/test/I2V/neverends",
+ "rgb": "data3/test/original/I2V/neverends"
+ }
+}
+
+# Model configurations - Only 3 variants for Table 2
+VARIANTS = {
+ "S_spatial": {
+ "eval_mode": "rgb_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "S_optical": {
+ "eval_mode": "optical_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "AIGVDet": {
+ "eval_mode": "fused",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ }
+}
+
+RESULTS_DIR = Path("data3/results/table2")
+RESULTS_DIR.mkdir(parents=True, exist_ok=True)
+
+def check_prerequisites():
+ """Check if all required datasets and models exist"""
+ print("="*80)
+ print("CHECKING PREREQUISITES FOR DATA3 (I2V)")
+ print("="*80)
+
+ # Check datasets
+ print("\n1. Checking I2V datasets...")
+ missing_data = []
+ for dataset_name, paths in DATASETS.items():
+ for stream_type, path in paths.items():
+ if not os.path.exists(path):
+ missing_data.append(f" ❌ {dataset_name} ({stream_type}): {path}")
+ else:
+ real_path = os.path.join(path, "0_real")
+ fake_path = os.path.join(path, "1_fake")
+
+ status = []
+ if os.path.exists(real_path):
+ status.append("Real ✓")
+ else:
+ status.append("Real ✗")
+
+ if os.path.exists(fake_path):
+ status.append("Fake ✓")
+ else:
+ status.append("Fake ✗")
+
+ print(f" ✓ {dataset_name} ({stream_type}): {path} [{', '.join(status)}]")
+
+ if not os.path.exists(real_path) and not os.path.exists(fake_path):
+ missing_data.append(f" ⚠️ {dataset_name} ({stream_type}): Missing BOTH 0_real and 1_fake folders")
+
+ if missing_data:
+ print("\n⚠️ Some data is missing:")
+ for item in missing_data:
+ print(item)
+
+ # Check models
+ print("\n2. Checking model checkpoints...")
+ models = ["checkpoints/optical.pth", "checkpoints/original.pth"]
+ missing_models = []
+ for model in models:
+ if os.path.exists(model):
+ print(f" ✓ {model}")
+ else:
+ print(f" ❌ {model}")
+ missing_models.append(model)
+
+ if missing_models:
+ print("\n❌ Missing models. Please ensure checkpoints are available.")
+ return False
+
+ if missing_data:
+ print("\n⚠️ Some datasets are incomplete but will continue...")
+
+ return True
+
+def run_evaluation(dataset_name, variant_name, variant_config, limit=None):
+ """Run evaluation for a specific dataset and variant"""
+ dataset_paths = DATASETS[dataset_name]
+
+ # Build command
+ cmd = [
+ "python", "test_single_stream.py",
+ "-fop", dataset_paths["optical"],
+ "-for", dataset_paths["rgb"],
+ "-mop", variant_config["optical_model"],
+ "-mor", variant_config["rgb_model"],
+ "--eval_mode", variant_config["eval_mode"],
+ "-e", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"),
+ "-ef", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_frame.csv"),
+ "-t", "0.5"
+ ]
+
+ if variant_config["no_crop"]:
+ cmd.append("--no_crop")
+
+ if limit:
+ cmd.extend(["--limit", str(limit)])
+
+ print(f"\n{'='*80}")
+ print(f"Running: {variant_name} on {dataset_name}")
+ print(f"{'='*80}")
+ print("Command:", " ".join(cmd))
+ print(f"{'='*80}\n")
+
+ # Run the command with real-time output
+ try:
+ # Use Popen to stream output in real-time
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ # Collect output while displaying it
+ output_lines = []
+ for line in process.stdout:
+ print(line, end='', flush=True) # Print in real-time
+ output_lines.append(line)
+
+ # Wait for process to complete
+ return_code = process.wait(timeout=3600)
+ output = ''.join(output_lines)
+
+ if return_code != 0:
+ print(f"\n⚠️ Command exited with code {return_code}")
+
+ # Parse metrics from CSV file
+ csv_path = RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"
+ metrics = parse_metrics(output, csv_path)
+ return metrics
+ except subprocess.TimeoutExpired:
+ print(f"\n⚠️ Timeout while running {variant_name} on {dataset_name}")
+ process.kill()
+ return None
+ except Exception as e:
+ print(f"\n❌ Error running {variant_name} on {dataset_name}: {e}")
+ return None
+
+def parse_metrics(output, csv_path):
+ """Parse metrics from CSV file (test_single_stream.py doesn't print to stdout)"""
+ try:
+ # Read the CSV file
+ if not os.path.exists(csv_path):
+ print(f" ⚠️ CSV file not found: {csv_path}")
+ return None
+
+ df = pd.read_csv(csv_path)
+
+ if len(df) == 0:
+ print(f" ⚠️ CSV file is empty: {csv_path}")
+ return None
+
+ # Calculate metrics from the CSV data
+ from sklearn.metrics import accuracy_score, roc_auc_score, average_precision_score
+
+ y_true = df['flag'].values # Ground truth (0=real, 1=fake)
+ y_pred = df['pro'].values # Predicted probability
+
+ # Calculate metrics
+ acc = accuracy_score(y_true, (y_pred >= 0.5).astype(int))
+ auc = roc_auc_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+ ap = average_precision_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+
+ metrics = {
+ 'acc': acc,
+ 'auc': auc,
+ 'ap': ap
+ }
+
+ print(f"\n ✓ Metrics: ACC={acc:.4f}, AUC={auc:.4f}, AP={ap:.4f}")
+ return metrics
+
+ except Exception as e:
+ print(f" ⚠️ Error reading metrics from CSV: {e}")
+ return None
+
+def run_all_evaluations(limit=None):
+ """Run evaluations for all variants and datasets"""
+ print("\n" + "="*80)
+ print("RUNNING EVALUATIONS FOR DATA3 (I2V)")
+ print("="*80)
+
+ all_results = {}
+
+ total_evals = len(VARIANTS) * len(DATASETS)
+ current_eval = 0
+
+ for variant_idx, (variant_name, variant_config) in enumerate(VARIANTS.items(), 1):
+ all_results[variant_name] = {}
+
+ print(f"\n{'='*80}")
+ print(f"VARIANT {variant_idx}/{len(VARIANTS)}: {variant_name}")
+ print(f"{'='*80}")
+
+ for dataset_idx, dataset_name in enumerate(DATASETS.keys(), 1):
+ current_eval += 1
+ overall_progress = (current_eval / total_evals) * 100
+
+ print(f"\n[Overall: {current_eval}/{total_evals} - {overall_progress:.1f}%]")
+ print(f"[Variant: {variant_idx}/{len(VARIANTS)}] [{variant_name}]")
+ print(f"[Dataset: {dataset_idx}/{len(DATASETS)}] [{dataset_name}]")
+
+ metrics = run_evaluation(dataset_name, variant_name, variant_config, limit)
+ all_results[variant_name][dataset_name] = metrics
+
+ return all_results
+
+def compile_table2(results):
+ """Compile results into Table 2 format"""
+ print("\n" + "="*80)
+ print("COMPILING TABLE 2 (DATA3 - I2V)")
+ print("="*80)
+
+ # Create DataFrame
+ rows = []
+ for dataset in DATASETS.keys():
+ row = {"Dataset": dataset}
+ for variant in VARIANTS.keys():
+ if results[variant][dataset]:
+ auc = results[variant][dataset].get('auc', 0) * 100
+ acc = results[variant][dataset].get('acc', 0) * 100
+ row[variant] = f"{auc:.1f}/{acc:.1f}"
+ else:
+ row[variant] = "N/A"
+ rows.append(row)
+
+ df = pd.DataFrame(rows)
+
+ # Save to CSV
+ output_file = RESULTS_DIR / "table2_data3_i2v.csv"
+ df.to_csv(output_file, index=False)
+
+ print(f"\n✓ Table 2 (I2V) saved to: {output_file}")
+ print("\nTable 2 Preview:")
+ print(df.to_string(index=False))
+
+ return df
+
+def main():
+ """Main execution function"""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--limit", type=int, default=None, help="Limit number of videos per class")
+ args = parser.parse_args()
+
+ print("="*80)
+ print("RECREATING TABLE 2 FOR DATA3 (I2V: MOONVALLEY, PIKA, NEVERENDS)")
+ print("="*80)
+ print("\nThis script will:")
+ print("1. Check prerequisites (data and models)")
+ print("2. Run evaluations for 3 variants (AIGVDet, Spatial, Optical)")
+ print("3. Compile results into Table 2 format")
+ print("="*80)
+
+ # Check prerequisites
+ if not check_prerequisites():
+ print("\n❌ Prerequisites not satisfied. Please fix the issues above.")
+ return
+
+ # Run all evaluations
+ all_results = run_all_evaluations(limit=args.limit)
+
+ # Compile and display Table 2
+ table = compile_table2(all_results)
+
+ print("\n" + "="*80)
+ print("✅ TABLE 2 (I2V) RECREATION COMPLETE!")
+ print("="*80)
+
+if __name__ == "__main__":
+ main()
diff --git a/recreate_table2_final.py b/recreate_table2_final.py
new file mode 100644
index 0000000..b68754d
--- /dev/null
+++ b/recreate_table2_final.py
@@ -0,0 +1,382 @@
+"""
+Comprehensive script to recreate Table 2 from the AIGVDet paper
+This script:
+1. Checks for required data and models
+2. Runs evaluations for all variants (Sspatial, Soptical, Soptical_no_cp, AIGVDet)
+3. Compiles results into Table 2 format
+"""
+
+import os
+import subprocess
+import pandas as pd
+from pathlib import Path
+import re
+import argparse
+
+# Configuration for datasets
+DATASETS = {
+ "moonvalley": {
+ "optical": "data/test/T2V/moonvalley",
+ "rgb": "data/test/original/T2V/moonvalley"
+ },
+ "videocraft": {
+ "optical": "data/test/T2V/videocraft",
+ "rgb": "data/test/original/T2V/videocraft"
+ },
+ "pika": {
+ "optical": "data/test/T2V/pika",
+ "rgb": "data/test/original/T2V/pika"
+ },
+ "neverends": {
+ "optical": "data/test/T2V/neverends",
+ "rgb": "data/test/original/T2V/neverends"
+ }
+}
+
+# Model configurations for each variant
+VARIANTS = {
+ "S_spatial": {
+ "eval_mode": "rgb_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "S_optical": {
+ "eval_mode": "optical_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "S_optical_no_cp": {
+ "eval_mode": "optical_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": True
+ },
+ "AIGVDet": {
+ "eval_mode": "fused",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ }
+}
+
+RESULTS_DIR = Path("data/results/table2")
+RESULTS_DIR.mkdir(parents=True, exist_ok=True)
+
+def check_prerequisites():
+ """Check if all required datasets and models exist"""
+ print("="*80)
+ print("CHECKING PREREQUISITES")
+ print("="*80)
+
+ # Check datasets
+ print("\n1. Checking datasets...")
+ missing_data = []
+ for dataset_name, paths in DATASETS.items():
+ for stream_type, path in paths.items():
+ if not os.path.exists(path):
+ missing_data.append(f" ❌ {dataset_name} ({stream_type}): {path}")
+ else:
+ # Check if has 0_real and 1_fake subfolders
+ real_path = os.path.join(path, "0_real")
+ fake_path = os.path.join(path, "1_fake")
+
+ status = []
+ if os.path.exists(real_path):
+ status.append("Real ✓")
+ else:
+ status.append("Real ✗")
+
+ if os.path.exists(fake_path):
+ status.append("Fake ✓")
+ else:
+ status.append("Fake ✗")
+
+ print(f" ✓ {dataset_name} ({stream_type}): {path} [{', '.join(status)}]")
+
+ if not os.path.exists(real_path) and not os.path.exists(fake_path):
+ missing_data.append(f" ⚠️ {dataset_name} ({stream_type}): Missing BOTH 0_real and 1_fake folders")
+
+ if missing_data:
+ print("\n⚠️ Critical issues found:")
+ for issue in missing_data:
+ print(issue)
+ print("\nPlease run prepare_data.py to extract frames from videos first.")
+ return False
+
+ # Check models
+ print("\n2. Checking model checkpoints...")
+ required_models = set()
+ for variant_config in VARIANTS.values():
+ required_models.add(variant_config["optical_model"])
+ required_models.add(variant_config["rgb_model"])
+
+ missing_models = []
+ for model_path in required_models:
+ if not os.path.exists(model_path):
+ missing_models.append(f" ❌ {model_path}")
+ else:
+ print(f" ✓ {model_path}")
+
+ if missing_models:
+ print("\n⚠️ Missing model checkpoints:")
+ for missing in missing_models:
+ print(missing)
+ print("\nPlease ensure you have trained models or download pre-trained checkpoints.")
+ return False
+
+ print("\n✓ All prerequisites satisfied!")
+ return True
+
+def run_evaluation(dataset_name, variant_name, variant_config, limit=None):
+ """
+ Run evaluation for a specific dataset and variant
+ """
+ dataset_paths = DATASETS[dataset_name]
+
+ # Build command
+ cmd = [
+ "python", "test_single_stream.py",
+ "-fop", dataset_paths["optical"],
+ "-for", dataset_paths["rgb"],
+ "-mop", variant_config["optical_model"],
+ "-mor", variant_config["rgb_model"],
+ "--eval_mode", variant_config["eval_mode"],
+ "-e", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"),
+ "-ef", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_frame.csv"),
+ "-t", "0.5"
+ ]
+
+ if variant_config["no_crop"]:
+ cmd.append("--no_crop")
+
+ if limit:
+ cmd.extend(["--limit", str(limit)])
+
+ print(f"\n{'='*80}")
+ print(f"Running: {variant_name} on {dataset_name}")
+ print(f"{'='*80}")
+ print("Command:", " ".join(cmd))
+ print(f"{'='*80}\n")
+
+ # Run the command with real-time output
+ try:
+ # Use Popen to stream output in real-time
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ # Collect output while displaying it
+ output_lines = []
+ for line in process.stdout:
+ print(line, end='', flush=True) # Print in real-time
+ output_lines.append(line)
+
+ # Wait for process to complete
+ return_code = process.wait(timeout=3600)
+ output = ''.join(output_lines)
+
+ if return_code != 0:
+ print(f"\n⚠️ Command exited with code {return_code}")
+
+ # Parse metrics from output
+ metrics = parse_metrics(output)
+ return metrics
+ except subprocess.TimeoutExpired:
+ print(f"\n⚠️ Timeout while running {variant_name} on {dataset_name}")
+ process.kill()
+ return None
+ except Exception as e:
+ print(f"\n❌ Error running {variant_name} on {dataset_name}: {e}")
+ return None
+
+def parse_metrics(output):
+ """Parse accuracy and AUC from test output"""
+ metrics = {'acc': None, 'auc': None}
+
+ lines = output.split('\n')
+ for line in lines:
+ # Look for "acc: 0.XXXX (XX.X%)" or "acc: 0.XXXX" or "Accuracy: XX.X%"
+ if 'acc' in line.lower():
+ # Try pattern 1: "acc: 0.XXXX"
+ match = re.search(r'acc[:\s]+([0-9.]+)', line, re.IGNORECASE)
+ if match:
+ metrics['acc'] = float(match.group(1))
+
+ # Look for "auc: 0.XXXX (XX.X%)" or "auc: 0.XXXX" or "AUC: XX.X%"
+ if 'auc' in line.lower():
+ # Try pattern 1: "auc: 0.XXXX"
+ match = re.search(r'auc[:\s]+([0-9.]+)', line, re.IGNORECASE)
+ if match:
+ metrics['auc'] = float(match.group(1))
+
+ # Debug output if metrics not found
+ if metrics['acc'] is None or metrics['auc'] is None:
+ print("\n ⚠️ Warning: Could not parse all metrics from output")
+ print(f" Found ACC: {metrics['acc']}, AUC: {metrics['auc']}")
+ print(" Last 10 lines of output:")
+ for line in lines[-10:]:
+ if line.strip():
+ print(f" {line}")
+
+ return metrics
+
+def run_all_evaluations(limit=None):
+ """Run evaluations for all variants and datasets"""
+ print("\n" + "="*80)
+ print("RUNNING EVALUATIONS")
+ print("="*80)
+
+ all_results = {}
+
+ # Calculate total number of evaluations
+ total_evals = len(VARIANTS) * len(DATASETS)
+ current_eval = 0
+
+ for variant_idx, (variant_name, variant_config) in enumerate(VARIANTS.items(), 1):
+ all_results[variant_name] = {}
+
+ print(f"\n{'='*80}")
+ print(f"VARIANT {variant_idx}/{len(VARIANTS)}: {variant_name}")
+ print(f"{'='*80}")
+
+ for dataset_idx, dataset_name in enumerate(DATASETS.keys(), 1):
+ current_eval += 1
+ overall_progress = (current_eval / total_evals) * 100
+
+ print(f"\n[Overall: {current_eval}/{total_evals} - {overall_progress:.1f}%]")
+ print(f"[Variant: {variant_idx}/{len(VARIANTS)}] [{variant_name}]")
+ print(f"[Dataset: {dataset_idx}/{len(DATASETS)}] [{dataset_name}]")
+
+ metrics = run_evaluation(dataset_name, variant_name, variant_config, limit=limit)
+ all_results[variant_name][dataset_name] = metrics
+
+ if metrics:
+ acc_str = f"{metrics['acc']*100:.1f}%" if metrics['acc'] is not None else "N/A"
+ auc_str = f"{metrics['auc']*100:.1f}%" if metrics['auc'] is not None else "N/A"
+ print(f" ✓ ACC: {acc_str}, AUC: {auc_str}")
+ else:
+ print(f" ✗ Failed to get results")
+
+ print(f"\n{'='*80}")
+ print(f"✓ ALL EVALUATIONS COMPLETE ({total_evals}/{total_evals})")
+ print(f"{'='*80}")
+
+ return all_results
+
+def compile_table2(all_results):
+ """
+ Compile all results into Table 2 format
+ """
+ print("\n" + "="*80)
+ print("TABLE 2: Ablation test results")
+ print("Format: ACC(%)/AUC(%)")
+ print("="*80)
+
+ # Create table data
+ table_data = []
+
+ for variant_name in ["S_spatial", "S_optical", "S_optical_no_cp", "AIGVDet"]:
+ row = {"Variants": variant_name}
+
+ acc_values = []
+ auc_values = []
+
+ for dataset_name in ["moonvalley", "videocraft", "pika", "neverends"]:
+ metrics = all_results.get(variant_name, {}).get(dataset_name)
+
+ if metrics:
+ acc_pct = metrics['acc'] * 100 if metrics['acc'] is not None else None
+ auc_pct = metrics['auc'] * 100 if metrics['auc'] is not None else None
+
+ acc_str = f"{acc_pct:.1f}" if acc_pct is not None else "N/A"
+ auc_str = f"{auc_pct:.1f}" if auc_pct is not None else "N/A"
+
+ result_str = f"{acc_str}/{auc_str}"
+
+ if acc_pct is not None: acc_values.append(acc_pct)
+ if auc_pct is not None: auc_values.append(auc_pct)
+ else:
+ result_str = "N/A"
+
+ # Map dataset name to column name
+ column_name = {
+ "moonvalley": "Moonvalley",
+ "videocraft": "VideoCraft",
+ "pika": "Pika",
+ "neverends": "NeverEnds"
+ }[dataset_name]
+
+ row[column_name] = result_str
+
+ # Calculate average
+ avg_acc_str = f"{sum(acc_values) / len(acc_values):.1f}" if acc_values else "N/A"
+ avg_auc_str = f"{sum(auc_values) / len(auc_values):.1f}" if auc_values else "N/A"
+ row["Average"] = f"{avg_acc_str}/{avg_auc_str}"
+
+ table_data.append(row)
+
+ # Create DataFrame
+ df = pd.DataFrame(table_data)
+ df = df[["Variants", "Moonvalley", "VideoCraft", "Pika", "NeverEnds", "Average"]]
+
+ # Display table
+ print("\n" + df.to_string(index=False))
+ print("\n" + "="*80)
+
+ # Save to CSV
+ output_path = RESULTS_DIR / "table2_recreation.csv"
+ df.to_csv(output_path, index=False)
+ print(f"\n✓ Table saved to: {output_path}")
+
+ return df
+
+def main():
+ parser = argparse.ArgumentParser(description="Recreate Table 2 from AIGVDet paper")
+ parser.add_argument("--skip-checks", action="store_true", help="Skip prerequisite checks")
+ parser.add_argument("--limit", type=int, default=None, help="Limit number of videos per class for quick testing")
+ args = parser.parse_args()
+
+ print("="*80)
+ print("RECREATING TABLE 2 FROM AI-GENERATED VIDEO DETECTION PAPER")
+ print("="*80)
+ print("\nThis script will:")
+ print("1. Check prerequisites (data and models)")
+ print("2. Run evaluations for all variants on all datasets")
+ print("3. Compile results into Table 2 format")
+
+ if args.limit:
+ print(f"⚠️ QUICK MODE: Limiting to {args.limit} videos per class")
+ else:
+ print("\nEstimated time: 30-60 minutes depending on dataset sizes")
+ print("="*80)
+
+ # Check prerequisites
+ if not args.skip_checks:
+ if not check_prerequisites():
+ print("\n❌ Prerequisites not satisfied. Please fix the issues above.")
+ return
+
+ # Run all evaluations
+ all_results = run_all_evaluations(limit=args.limit)
+
+ # Compile and display Table 2
+ table = compile_table2(all_results)
+
+ print("\n" + "="*80)
+ print("✓ TABLE 2 RECREATION COMPLETE!")
+ print("="*80)
+ print(f"\nResults saved to: {RESULTS_DIR}")
+ print("\nFiles generated:")
+ print(f" - table2_recreation.csv (summary table)")
+ print(f" - [dataset]_[variant]_video.csv (per-video results)")
+ print(f" - [dataset]_[variant]_frame.csv (per-frame results)")
+
+if __name__ == "__main__":
+ main()
diff --git a/requirements.txt b/requirements.txt
index ce83367..8b5b0b5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ einops
imageio
ipympl
matplotlib
+natsort
numpy
opencv-python
pandas
@@ -12,3 +13,5 @@ tensorboard
tensorboardX
tqdm
blobfile>=1.0.5
+wandb
+python-dotenv
diff --git a/requirements.txt.backup b/requirements.txt.backup
new file mode 100644
index 0000000..ce83367
--- /dev/null
+++ b/requirements.txt.backup
@@ -0,0 +1,14 @@
+# conda create -n aigvdet python=3.9
+# pip install torch==2.0.0+cu117 torchvision==0.15.1+cu117 -f https://download.pytorch.org/whl/torch_stable.html
+einops
+imageio
+ipympl
+matplotlib
+numpy
+opencv-python
+pandas
+scikit-learn
+tensorboard
+tensorboardX
+tqdm
+blobfile>=1.0.5
diff --git a/run_gui.bat b/run_gui.bat
new file mode 100644
index 0000000..c2011a1
--- /dev/null
+++ b/run_gui.bat
@@ -0,0 +1,16 @@
+@echo off
+echo Starting AIGVDet GUI...
+echo.
+echo Checking if streamlit is installed...
+python -c "import streamlit" 2>nul
+if errorlevel 1 (
+ echo Streamlit not found. Installing required packages...
+ pip install streamlit torch torchvision opencv-python numpy pillow natsort tqdm
+)
+
+echo.
+echo Starting GUI on http://localhost:8501
+echo Press Ctrl+C to stop
+echo.
+streamlit run gui_app.py
+pause
diff --git a/run_gui.sh b/run_gui.sh
new file mode 100644
index 0000000..9581029
--- /dev/null
+++ b/run_gui.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Run AIGVDet GUI locally (without Docker)
+
+echo "Starting AIGVDet GUI..."
+echo ""
+
+# Check if streamlit is installed
+if ! python -c "import streamlit" 2>/dev/null; then
+ echo "Streamlit not found. Installing required packages..."
+ pip install streamlit torch torchvision opencv-python numpy pillow natsort tqdm
+fi
+
+echo ""
+echo "Starting GUI on http://localhost:8501"
+echo "Press Ctrl+C to stop"
+echo ""
+
+streamlit run gui_app.py
diff --git a/run_gui_docker.bat b/run_gui_docker.bat
new file mode 100644
index 0000000..95f59ae
--- /dev/null
+++ b/run_gui_docker.bat
@@ -0,0 +1,14 @@
+@echo off
+REM Run AIGVDet GUI in Docker (Windows)
+
+echo Starting AIGVDet GUI...
+echo Installing Streamlit and starting server...
+echo Access the GUI at: http://localhost:8501
+echo.
+
+docker run --gpus all -p 8501:8501 -it ^
+ -v %cd%/output_data:/app/output_data ^
+ -v %cd%/checkpoints:/app/checkpoints ^
+ -v %cd%/raft-model:/app/raft-model ^
+ sacdalance/thesis-aigvdet:latest-gpu ^
+ streamlit run gui_app.py --server.address=0.0.0.0
diff --git a/run_gui_docker.sh b/run_gui_docker.sh
new file mode 100644
index 0000000..7e0223b
--- /dev/null
+++ b/run_gui_docker.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+# Run AIGVDet GUI in Docker
+
+echo "Starting AIGVDet GUI..."
+echo "Installing Streamlit and starting server..."
+echo "Access the GUI at: http://localhost:8501"
+echo ""
+
+docker run --gpus all -p 8501:8501 -it \
+ -v $(pwd)/output_data:/app/output_data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/raft-model:/app/raft-model \
+ sacdalance/thesis-aigvdet:latest-gpu \
+ streamlit run gui_app.py --server.address=0.0.0.0
diff --git a/setup_test_data.py b/setup_test_data.py
new file mode 100644
index 0000000..701a101
--- /dev/null
+++ b/setup_test_data.py
@@ -0,0 +1,254 @@
+"""
+Simple script to download and extract test data for AIGVDet evaluation
+Google Drive folder: https://drive.google.com/drive/u/3/folders/1gSAUUqYK33262aukdTjZIxUuGrgU8REU
+
+This script:
+1. Downloads the test folder from Google Drive using gdown
+2. Extracts all zip files to the correct locations
+3. Organizes data structure for recreate_table2_final.py
+"""
+
+import os
+import subprocess
+import zipfile
+from pathlib import Path
+import sys
+import shutil
+
+# Google Drive folder link
+GDRIVE_FOLDER = "https://drive.google.com/drive/folders/1D1jm1_HCu0Nv21NVjuyL1CB5gF5sy0hx"
+
+# Paths
+DATA_DIR = Path("data")
+TEST_DIR = DATA_DIR / "test"
+TEMP_DIR = DATA_DIR / "temp_download"
+
+def install_gdown():
+ """Install gdown if not already installed"""
+ print("Checking for gdown...")
+ try:
+ import gdown
+ print("✓ gdown is already installed")
+ return True
+ except ImportError:
+ print("Installing gdown...")
+ try:
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "gdown"])
+ print("✓ gdown installed successfully")
+ return True
+ except Exception as e:
+ print(f"❌ Failed to install gdown: {e}")
+ return False
+
+def download_data():
+ """Download test data from Google Drive"""
+ print(f"\n{'='*80}")
+ print("DOWNLOADING TEST DATA")
+ print(f"{'='*80}")
+ print(f"Source: {GDRIVE_FOLDER}")
+ print(f"Destination: {TEMP_DIR}")
+
+ # Create temp directory
+ TEMP_DIR.mkdir(parents=True, exist_ok=True)
+
+ # Check if files already exist
+ existing_zips = list(TEMP_DIR.rglob("*.zip"))
+ if existing_zips:
+ print(f"\n⚠️ Found {len(existing_zips)} existing zip files in {TEMP_DIR}")
+ print("Skipping download. If you want to re-download, delete the temp_download folder first.")
+ return True
+
+ # Try method 1: gdown with cookies
+ try:
+ import gdown
+ print("\nMethod 1: Trying download with cookies authentication...")
+ gdown.download_folder(GDRIVE_FOLDER, output=str(TEMP_DIR), quiet=False, use_cookies=True)
+ print("\n✓ Download complete")
+ return True
+ except Exception as e:
+ print(f"\n⚠️ Method 1 failed: {e}")
+
+ # Try method 2: gdown without cookies
+ try:
+ import gdown
+ print("\nMethod 2: Trying download without cookies...")
+ gdown.download_folder(GDRIVE_FOLDER, output=str(TEMP_DIR), quiet=False, use_cookies=False)
+ print("\n✓ Download complete")
+ return True
+ except Exception as e:
+ print(f"\n⚠️ Method 2 failed: {e}")
+
+ # Try method 3: Command line with cookies
+ try:
+ print("\nMethod 3: Trying command line with cookies...")
+ cmd = ["gdown", "--folder", GDRIVE_FOLDER, "-O", str(TEMP_DIR), "--use-cookies"]
+ subprocess.run(cmd, check=True)
+ print("\n✓ Download complete")
+ return True
+ except Exception as e:
+ print(f"\n⚠️ Method 3 failed: {e}")
+
+ # All methods failed
+ print("\n" + "="*80)
+ print("❌ AUTOMATIC DOWNLOAD FAILED - MANUAL DOWNLOAD REQUIRED")
+ print("="*80)
+ print("\nGoogle Drive has rate-limited downloads. Please download manually:")
+ print(f"\n1. Open in browser: {GDRIVE_FOLDER}")
+ print("2. Download all files/folders")
+ print(f"3. Extract to: {TEMP_DIR.absolute()}")
+ print("4. Run this script again")
+ print("\nAlternatively, wait 24 hours and try again.")
+ print("="*80)
+ return False
+
+def extract_zip(zip_path, extract_to):
+ """Extract a single zip file"""
+ print(f" Extracting: {zip_path.name} → {extract_to.name}")
+ try:
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ zip_ref.extractall(extract_to)
+ return True
+ except Exception as e:
+ print(f" ❌ Error: {e}")
+ return False
+
+def organize_data():
+ """Extract and organize all zip files"""
+ print(f"\n{'='*80}")
+ print("EXTRACTING AND ORGANIZING DATA")
+ print(f"{'='*80}")
+
+ # Create target directories
+ optical_base = TEST_DIR / "T2V"
+ rgb_base = TEST_DIR / "original" / "T2V"
+ optical_base.mkdir(parents=True, exist_ok=True)
+ rgb_base.mkdir(parents=True, exist_ok=True)
+
+ # Find all zip files in temp directory
+ all_zips = list(TEMP_DIR.rglob("*.zip"))
+
+ if not all_zips:
+ print("❌ No zip files found in downloaded data")
+ return False
+
+ print(f"\nFound {len(all_zips)} zip files")
+
+ # Process optical flow zips
+ print("\n1. Processing optical flow data...")
+ optical_zips = [z for z in all_zips if "-optical" in z.name.lower()]
+ for zip_file in optical_zips:
+ # Extract dataset name (e.g., "videocraft" from "videocraft-optical.zip")
+ dataset_name = zip_file.stem.replace("-optical", "").replace("-Optical", "")
+ target_dir = optical_base / dataset_name
+ target_dir.mkdir(parents=True, exist_ok=True)
+ extract_zip(zip_file, target_dir)
+
+ # Process RGB zips
+ print("\n2. Processing RGB data...")
+ rgb_zips = [z for z in all_zips if "-rgb" in z.name.lower()]
+ for zip_file in rgb_zips:
+ # Extract dataset name (e.g., "videocraft" from "videocraft-rgb.zip")
+ dataset_name = zip_file.stem.replace("-rgb", "").replace("-RGB", "")
+ target_dir = rgb_base / dataset_name
+ target_dir.mkdir(parents=True, exist_ok=True)
+ extract_zip(zip_file, target_dir)
+
+ print("\n✓ Extraction complete")
+ return True
+
+def verify_structure():
+ """Verify the extracted data structure"""
+ print(f"\n{'='*80}")
+ print("VERIFYING DATA STRUCTURE")
+ print(f"{'='*80}")
+
+ datasets = ["moonvalley", "videocraft", "pika", "neverends"]
+ all_good = True
+
+ print("\nOptical flow data:")
+ for dataset in datasets:
+ path = TEST_DIR / "T2V" / dataset
+ if path.exists():
+ real = (path / "0_real").exists()
+ fake = (path / "1_fake").exists()
+ status = f"[Real: {'✓' if real else '✗'}, Fake: {'✓' if fake else '✗'}]"
+ print(f" {'✓' if (real and fake) else '⚠️ '} {dataset}: {status}")
+ if not (real and fake):
+ all_good = False
+ else:
+ print(f" ❌ {dataset}: NOT FOUND")
+ all_good = False
+
+ print("\nRGB data:")
+ for dataset in datasets:
+ path = TEST_DIR / "original" / "T2V" / dataset
+ if path.exists():
+ real = (path / "0_real").exists()
+ fake = (path / "1_fake").exists()
+ status = f"[Real: {'✓' if real else '✗'}, Fake: {'✓' if fake else '✗'}]"
+ print(f" {'✓' if (real and fake) else '⚠️ '} {dataset}: {status}")
+ if not (real and fake):
+ all_good = False
+ else:
+ print(f" ❌ {dataset}: NOT FOUND")
+ all_good = False
+
+ return all_good
+
+def cleanup():
+ """Remove temporary files"""
+ print(f"\n{'='*80}")
+ print("CLEANUP")
+ print(f"{'='*80}")
+
+ if TEMP_DIR.exists():
+ try:
+ shutil.rmtree(TEMP_DIR)
+ print(f"✓ Removed temporary directory: {TEMP_DIR}")
+ except Exception as e:
+ print(f"⚠️ Could not remove temp directory: {e}")
+ print(f"You can manually delete: {TEMP_DIR}")
+
+def main():
+ print("="*80)
+ print("AIGVDET TEST DATA SETUP")
+ print("="*80)
+
+ # Step 1: Install gdown
+ if not install_gdown():
+ print("\n❌ Setup failed: Could not install gdown")
+ return
+
+ # Step 2: Download data
+ if not download_data():
+ print("\n❌ Setup failed: Could not download data")
+ return
+
+ # Step 3: Extract and organize
+ if not organize_data():
+ print("\n❌ Setup failed: Could not extract data")
+ return
+
+ # Step 4: Verify structure
+ success = verify_structure()
+
+ # Step 5: Cleanup
+ cleanup()
+
+ # Final message
+ print(f"\n{'='*80}")
+ if success:
+ print("✓ SETUP COMPLETE!")
+ print("="*80)
+ print("\nAll test data is ready!")
+ print("\nNext step:")
+ print(" python recreate_table2_final.py")
+ else:
+ print("⚠️ SETUP COMPLETED WITH WARNINGS")
+ print("="*80)
+ print("\nSome data may be missing. Please check the warnings above.")
+ print("You may need to manually extract some files.")
+ print("="*80)
+
+if __name__ == "__main__":
+ main()
diff --git a/test.py b/test.py
index e25e90b..804e1cc 100644
--- a/test.py
+++ b/test.py
@@ -64,6 +64,7 @@
parser.add_argument("--use_cpu", action="store_true", help="uses gpu by default, turn on to use cpu")
parser.add_argument("--arch", type=str, default="resnet50")
parser.add_argument("--aug_norm", type=str2bool, default=True)
+ parser.add_argument("--no_crop", action="store_true", help="disable center crop")
args = parser.parse_args()
subfolder_count = 0
@@ -88,12 +89,19 @@
model_or.cuda()
- trans = transforms.Compose(
- (
- transforms.CenterCrop((448,448)),
- transforms.ToTensor(),
+ if args.no_crop:
+ trans = transforms.Compose(
+ (
+ transforms.ToTensor(),
+ )
+ )
+ else:
+ trans = transforms.Compose(
+ (
+ transforms.CenterCrop((448,448)),
+ transforms.ToTensor(),
+ )
)
- )
print("*" * 50)
@@ -104,6 +112,8 @@
tn=0
y_true=[]
y_pred=[]
+ y_pred_original=[]
+ y_pred_optical=[]
# create an empty DataFrame
df = pd.DataFrame(columns=['name', 'pro','flag','optical_pro','original_pro'])
@@ -130,7 +140,8 @@
print("test subfolder:", subfolder_name)
# Traverse through sub-subfolders within a subfolder.
- for subsubfolder_name in os.listdir(original_subfolder_path):
+ video_list = os.listdir(original_subfolder_path)
+ for subsubfolder_name in tqdm(video_list, desc=f"Testing {subfolder_name}"):
original_subsubfolder_path = os.path.join(original_subfolder_path, subsubfolder_name)
optical_subsubfolder_path = os.path.join(optical_subfolder_path, subsubfolder_name)
if os.path.isdir(optical_subsubfolder_path):
@@ -145,7 +156,8 @@
original_file_list = sorted(glob.glob(os.path.join(original_subsubfolder_path, "*.jpg")) + glob.glob(os.path.join(original_subsubfolder_path, "*.png"))+glob.glob(os.path.join(original_subsubfolder_path, "*.JPEG")))
original_prob_sum=0
- for img_path in tqdm(original_file_list, dynamic_ncols=True, disable=len(original_file_list) <= 1):
+ # Inner loop for frames - disable tqdm to avoid nested clutter
+ for img_path in original_file_list:
img = Image.open(img_path).convert("RGB")
img = trans(img)
@@ -159,16 +171,17 @@
prob = model_or(in_tens).sigmoid().item()
original_prob_sum+=prob
- df1 = df1.append({'original_path': img_path, 'original_pro': prob , 'flag':flag}, ignore_index=True)
+ df1 = pd.concat([df1, pd.DataFrame([{'original_path': img_path, 'original_pro': prob , 'flag':flag}])], ignore_index=True)
original_predict=original_prob_sum/len(original_file_list)
- print("original prob",original_predict)
+ # print("original prob",original_predict)
#Detect optical flow
optical_file_list = sorted(glob.glob(os.path.join(optical_subsubfolder_path, "*.jpg")) + glob.glob(os.path.join(optical_subsubfolder_path, "*.png"))+glob.glob(os.path.join(optical_subsubfolder_path, "*.JPEG")))
optical_prob_sum=0
- for img_path in tqdm(optical_file_list, dynamic_ncols=True, disable=len(original_file_list) <= 1):
+ # Inner loop for frames - disable tqdm
+ for img_path in optical_file_list:
img = Image.open(img_path).convert("RGB")
img = trans(img)
@@ -188,13 +201,15 @@
index1=index1+1
optical_predict=optical_prob_sum/len(optical_file_list)
- print("optical prob",optical_predict)
+ # print("optical prob",optical_predict)
predict=original_predict*0.5+optical_predict*0.5
- print(f"flag:{flag} predict:{predict}")
+ # print(f"flag:{flag} predict:{predict}")
# y_true.append((float)(flag))
y_true.append((flag))
y_pred.append(predict)
+ y_pred_original.append(original_predict)
+ y_pred_optical.append(optical_predict)
if flag==0:
n+=1
if predict=args.threshold:
tp+=1
- df = df.append({'name': subsubfolder_name, 'pro': predict , 'flag':flag ,'optical_pro':optical_predict,'original_pro':original_predict}, ignore_index=True)
+ df = pd.concat([df, pd.DataFrame([{'name': subsubfolder_name, 'pro': predict , 'flag':flag ,'optical_pro':optical_predict,'original_pro':original_predict}])], ignore_index=True)
else:
print("Subfolder does not exist:", original_subfolder_path)
# r_acc = accuracy_score(y_true[y_true == 0], y_pred[y_true == 0] > args.threshold)
@@ -212,14 +227,33 @@
ap = average_precision_score(y_true, y_pred)
auc=roc_auc_score(y_true,y_pred)
- # print(f"r_acc:{r_acc}")
+ acc = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred])
+
+ # Calculate metrics for individual streams
+ acc_original = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred_original])
+ auc_original = roc_auc_score(y_true, y_pred_original)
+
+ acc_optical = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred_optical])
+ auc_optical = roc_auc_score(y_true, y_pred_optical)
+
+ print("-" * 30)
+ print("AIGVDet (Fused) Results:")
print(f"tnr:{tn/n}")
- # print(f"f_acc:{f_acc}")
print(f"tpr:{tp/p}")
- print(f"acc:{(tp+tn)/(p+n)}")
- # print(f"acc:{acc}")
+ print(f"acc:{acc}")
print(f"ap:{ap}")
print(f"auc:{auc}")
+
+ print("-" * 30)
+ print("Sspatial (Original RGB) Results:")
+ print(f"acc:{acc_original}")
+ print(f"auc:{auc_original}")
+
+ print("-" * 30)
+ print("Soptical (Optical Flow) Results:")
+ print(f"acc:{acc_optical}")
+ print(f"auc:{auc_optical}")
+ print("-" * 30)
print(f"p:{p}")
print(f"n:{n}")
print(f"tp:{tp}")
diff --git a/test.sh b/test.sh
deleted file mode 100644
index c870a97..0000000
--- a/test.sh
+++ /dev/null
@@ -1 +0,0 @@
-python test.py -fop "data/test/T2V/hotshot" -mop "checkpoints/optical_aug.pth" -for "data/test/original/T2V/hotshot" -mor "checkpoints/original_aug.pth" -e "data/results/T2V/hotshot.csv" -ef "data/results/frame/T2V/hotshot.csv" -t 0.5
\ No newline at end of file
diff --git a/test_single_stream.py b/test_single_stream.py
new file mode 100644
index 0000000..d401104
--- /dev/null
+++ b/test_single_stream.py
@@ -0,0 +1,333 @@
+"""
+Modified test.py that supports single-stream evaluation AND flat directory structures.
+Supports: RGB-only (Sspatial), Optical-only (Soptical), or Fused (AIGVDet)
+"""
+import argparse
+import glob
+import os
+import pandas as pd
+import re
+
+import torch
+import torch.nn
+import torchvision.transforms as transforms
+import torchvision.transforms.functional as TF
+from PIL import Image
+from tqdm import tqdm
+
+from core.utils1.utils import get_network, str2bool, to_cuda
+from sklearn.metrics import accuracy_score, average_precision_score, roc_auc_score
+
+def get_video_name_from_filename(filename):
+ # Assumes format: video_name_XXXXX.png
+ # We split by underscore and take everything except the last part (frame number)
+ parts = os.path.basename(filename).rsplit('_', 1)
+ if len(parts) > 1:
+ return parts[0]
+ return "unknown_video"
+
+if __name__=="__main__":
+
+ parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument("-fop", "--folder_optical_flow_path", default="data/test/T2V/videocraft", type=str)
+ parser.add_argument("-for", "--folder_original_path", default="data/test/original/T2V/videocraft", type=str)
+ parser.add_argument("-mop", "--model_optical_flow_path", type=str, default="checkpoints/optical.pth")
+ parser.add_argument("-mor", "--model_original_path", type=str, default="checkpoints/original.pth")
+ parser.add_argument("--eval_mode", type=str, choices=["fused", "rgb_only", "optical_only"], default="fused")
+ parser.add_argument("-t", "--threshold", type=float, default=0.5)
+ parser.add_argument("-e", "--excel_path", type=str, default="data/results/result.csv")
+ parser.add_argument("-ef", "--excel_frame_path", type=str, default="data/results/frame_result.csv")
+ parser.add_argument("--use_cpu", action="store_true")
+ parser.add_argument("--arch", type=str, default="resnet50")
+ parser.add_argument("--aug_norm", type=str2bool, default=True)
+ parser.add_argument("--no_crop", action="store_true")
+ parser.add_argument("--limit", type=int, default=None, help="Limit number of videos per class (optional)")
+
+ args = parser.parse_args()
+
+ # Load models
+ if args.eval_mode in ["fused", "optical_only"]:
+ print(f"Loading optical flow model: {args.model_optical_flow_path}")
+ model_op = get_network(args.arch)
+ state_dict = torch.load(args.model_optical_flow_path, map_location="cpu")
+ if "model" in state_dict: state_dict = state_dict["model"]
+ model_op.load_state_dict(state_dict)
+ model_op.eval()
+ if not args.use_cpu: model_op.cuda()
+ else: model_op = None
+
+ if args.eval_mode in ["fused", "rgb_only"]:
+ print(f"Loading RGB model: {args.model_original_path}")
+ model_or = get_network(args.arch)
+ state_dict = torch.load(args.model_original_path, map_location="cpu")
+ if "model" in state_dict: state_dict = state_dict["model"]
+ model_or.load_state_dict(state_dict)
+ model_or.eval()
+ if not args.use_cpu: model_or.cuda()
+ else: model_or = None
+
+ if args.no_crop:
+ trans = transforms.Compose((transforms.ToTensor(),))
+ else:
+ trans = transforms.Compose((transforms.CenterCrop((448,448)), transforms.ToTensor(),))
+
+ print("*" * 50)
+ print(f"Evaluation Mode: {args.eval_mode}")
+ print("*" * 50)
+
+ flag=0
+ p=0; n=0; tp=0; tn=0
+ y_true=[]; y_pred=[]
+ y_pred_original=[]; y_pred_optical=[]
+
+ df = pd.DataFrame(columns=['name', 'pro','flag','optical_pro','original_pro'])
+ df1 = pd.DataFrame(columns=['original_path', 'original_pro','optical_path','optical_pro','flag'])
+ index1=0
+
+ # Check if standard structure (0_real/1_fake) exists
+ has_standard_structure = os.path.exists(os.path.join(args.folder_original_path, "1_fake")) or \
+ os.path.exists(os.path.join(args.folder_optical_flow_path, "1_fake"))
+
+ if has_standard_structure:
+ print("Detected standard folder structure (0_real/1_fake)")
+ subfolders = ["0_real", "1_fake"]
+ else:
+ print("Detected FLAT folder structure (treating all as 1_fake)")
+ subfolders = ["flat_fake"]
+
+ for subfolder_name in subfolders:
+ if subfolder_name == "0_real":
+ flag = 0
+ current_label_path = "0_real"
+ elif subfolder_name == "1_fake":
+ flag = 1
+ current_label_path = "1_fake"
+ else:
+ flag = 1 # Flat structure assumed to be fake/generated videos
+ current_label_path = "" # Root dir
+
+ optical_subfolder_path = os.path.join(args.folder_optical_flow_path, current_label_path)
+ original_subfolder_path = os.path.join(args.folder_original_path, current_label_path)
+
+ # Get list of videos
+ # In flat structure, we need to group images by video prefix
+ video_groups = {}
+
+ if args.eval_mode != "optical_only":
+ # Scan RGB folder
+ if os.path.exists(original_subfolder_path):
+ files = sorted(glob.glob(os.path.join(original_subfolder_path, "*.png")) +
+ glob.glob(os.path.join(original_subfolder_path, "*.jpg")) +
+ glob.glob(os.path.join(original_subfolder_path, "*.JPEG")))
+
+ # If files found directly, it's flat structure
+ if len(files) > 0 and not os.path.isdir(files[0]):
+ for f in files:
+ vname = get_video_name_from_filename(f)
+ if vname not in video_groups: video_groups[vname] = []
+ video_groups[vname].append(f)
+ else:
+ # Standard structure: folders are videos
+ try:
+ video_list = os.listdir(original_subfolder_path)
+ for v in video_list:
+ v_path = os.path.join(original_subfolder_path, v)
+ if os.path.isdir(v_path):
+ frames = sorted(glob.glob(os.path.join(v_path, "*")))
+ if frames:
+ video_groups[v] = frames
+ except Exception as e:
+ print(f"Error listing directory: {e}")
+
+ # If optical_only, we MUST scan the optical folder
+ if args.eval_mode == "optical_only":
+ if os.path.exists(optical_subfolder_path):
+ files = sorted(glob.glob(os.path.join(optical_subfolder_path, "*.png")) +
+ glob.glob(os.path.join(optical_subfolder_path, "*.jpg")) +
+ glob.glob(os.path.join(optical_subfolder_path, "*.JPEG")))
+
+ if len(files) > 0 and not os.path.isdir(files[0]):
+ for f in files:
+ vname = get_video_name_from_filename(f)
+ if vname not in video_groups: video_groups[vname] = []
+ video_groups[vname].append(f)
+ else:
+ try:
+ video_list = os.listdir(optical_subfolder_path)
+ for v in video_list:
+ v_path = os.path.join(optical_subfolder_path, v)
+ if os.path.isdir(v_path):
+ frames = sorted(glob.glob(os.path.join(v_path, "*")))
+ if frames:
+ video_groups[v] = frames
+ except Exception as e:
+ print(f"Error listing directory: {e}")
+
+ # If optical only or fused, we might need to check optical folder too
+ # But usually RGB folder structure defines the videos.
+
+ print(f"Found {len(video_groups)} videos in {subfolder_name}")
+
+ video_list = list(video_groups.items())
+
+ # Apply limit if specified
+ if args.limit is not None and len(video_list) > args.limit:
+ print(f"⚠️ Limiting to first {args.limit} videos (out of {len(video_list)})")
+ video_list = video_list[:args.limit]
+
+ for idx, (video_name, frames) in enumerate(video_list, 1):
+ progress_pct = (idx / len(video_list)) * 100
+ print(f"\r[{idx}/{len(video_list)} - {progress_pct:.1f}%] Processing: {video_name[:50]}...", end='', flush=True)
+
+ # Detect RGB stream
+ original_predict = 0
+ if args.eval_mode in ["fused", "rgb_only"] and model_or is not None:
+ original_prob_sum=0
+ count = 0
+ for img_path in frames:
+ try:
+ img = Image.open(img_path).convert("RGB")
+ img = trans(img)
+ if args.aug_norm:
+ img = TF.normalize(img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ in_tens = img.unsqueeze(0)
+ if not args.use_cpu: in_tens = in_tens.cuda()
+
+ with torch.no_grad():
+ prob = model_or(in_tens).sigmoid().item()
+ original_prob_sum+=prob
+
+ df1 = pd.concat([df1, pd.DataFrame([{'original_path': img_path, 'original_pro': prob , 'flag':flag}])], ignore_index=True)
+ count += 1
+ except Exception as e:
+ print(f"Error processing RGB frame {img_path}: {e}")
+
+ if count > 0:
+ original_predict = original_prob_sum/count
+
+ # Detect Optical Flow stream
+ optical_predict = 0
+ if args.eval_mode in ["fused", "optical_only"] and model_op is not None:
+ # Construct optical flow paths
+ # In flat structure: optical_path/video_name_XXXX.png
+ # In standard: optical_path/video_name/frame_XXXX.png
+
+ optical_prob_sum=0
+ count = 0
+
+ # We iterate through the SAME frames as RGB to ensure alignment
+ # But we need to find the corresponding optical flow file
+ for img_path in frames:
+ basename = os.path.basename(img_path)
+
+ # Construct optical flow path
+ # Try standard path: .../1_fake/video_name/frame.png
+ opt_path_standard = os.path.join(optical_subfolder_path, video_name, basename)
+ # Try flat path: .../1_fake/frame.png
+ opt_path_flat = os.path.join(optical_subfolder_path, basename)
+
+ if os.path.exists(opt_path_standard):
+ opt_path = opt_path_standard
+ elif os.path.exists(opt_path_flat):
+ opt_path = opt_path_flat
+ else:
+ # Fallback to standard for error reporting, or try root if flat structure
+ if current_label_path == "":
+ opt_path = os.path.join(optical_subfolder_path, basename)
+ else:
+ opt_path = opt_path_standard
+
+ if os.path.exists(opt_path):
+ try:
+ img = Image.open(opt_path).convert("RGB")
+ img = trans(img)
+ if args.aug_norm:
+ img = TF.normalize(img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ in_tens = img.unsqueeze(0)
+ if not args.use_cpu: in_tens = in_tens.cuda()
+
+ with torch.no_grad():
+ prob = model_op(in_tens).sigmoid().item()
+ optical_prob_sum+=prob
+
+ df1.loc[index1, 'optical_path'] = opt_path
+ df1.loc[index1, 'optical_pro'] = prob
+ index1+=1
+ count+=1
+ except Exception as e:
+ print(f"Error processing Flow frame {opt_path}: {e}")
+ index1+=1
+ else:
+ # Flow frame missing
+ index1+=1
+
+ if count > 0:
+ optical_predict = optical_prob_sum/count
+
+ # Final Prediction
+ if args.eval_mode == "fused":
+ predict = original_predict * 0.5 + optical_predict * 0.5
+ elif args.eval_mode == "rgb_only":
+ predict = original_predict
+ else:
+ predict = optical_predict
+
+ y_true.append(flag)
+ y_pred.append(predict)
+ y_pred_original.append(original_predict)
+ y_pred_optical.append(optical_predict)
+
+ if flag==0:
+ n+=1
+ if predict=args.threshold: tp+=1
+
+ df = pd.concat([df, pd.DataFrame([{'name': video_name, 'pro': predict , 'flag':flag ,
+ 'optical_pro':optical_predict,'original_pro':original_predict}])], ignore_index=True)
+
+ # Print newline after completing subfolder
+ print(f"\n✓ Completed {subfolder_name}: {len(video_list)} videos processed")
+
+ # Metrics
+ # Metrics
+ try:
+ if len(y_true) == 0:
+ print("Error: No videos were processed. Cannot calculate metrics.")
+ ap = 0.0; auc = 0.0; acc = 0.0
+ elif len(set(y_true)) > 1:
+ ap = average_precision_score(y_true, y_pred)
+ auc = roc_auc_score(y_true,y_pred)
+ acc = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred])
+ else:
+ ap = 0.0; auc = 0.0
+ # Calculate accuracy even if only one class
+ acc = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred])
+ print(f"Warning: Only one class present (Class {y_true[0]}). AUC/AP cannot be calculated.")
+ except Exception as e:
+ print(f"Error calculating metrics: {e}")
+ ap=0.0; auc=0.0; acc=0.0
+
+ print("-" * 30)
+ print(f"Evaluation Mode: {args.eval_mode}")
+ print(f"tnr: {tn/n if n > 0 else 0:.4f}")
+ print(f"tpr: {tp/p if p > 0 else 0:.4f}")
+ print(f"acc: {acc:.4f}")
+ print(f"auc: {auc:.4f}")
+ print("-" * 30)
+
+ # Save results
+ csv_folder = os.path.dirname(args.excel_path)
+ if not os.path.exists(csv_folder): os.makedirs(csv_folder)
+
+ if not os.path.exists(args.excel_path): df.to_csv(args.excel_path, index=False)
+ else: df.to_csv(args.excel_path, mode='a', header=False, index=False)
+
+ csv_folder1 = os.path.dirname(args.excel_frame_path)
+ if not os.path.exists(csv_folder1): os.makedirs(csv_folder1)
+
+ if not os.path.exists(args.excel_frame_path): df1.to_csv(args.excel_frame_path, index=False)
+ else: df1.to_csv(args.excel_frame_path, mode='a', header=False, index=False)
+
+ print(f"Results saved to {args.excel_path}")
diff --git a/train.py b/train.py
index 4792f2e..956099f 100644
--- a/train.py
+++ b/train.py
@@ -3,41 +3,88 @@
import os
import time
+from dotenv import load_dotenv
from tensorboardX import SummaryWriter
from tqdm import tqdm
+import wandb
from core.utils1.datasets import create_dataloader
from core.utils1.earlystop import EarlyStopping
from core.utils1.eval import get_val_cfg, validate
-from core.utils1.trainer import Trainer
+from core.utils1.trainer import Trainer # change
from core.utils1.utils import Logger
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
+# Load environment variables from .env file
+load_dotenv('thesis.env')
+
+# Set wandb API key from environment
+wandb_api_key = os.getenv("WANDB")
+if wandb_api_key:
+ os.environ["WANDB"] = wandb_api_key
+ print(f"✓ Loaded wandb API key from .env")
+else:
+ print("⚠️ Warning: WANDB API key not found in .env file")
+
if __name__ == "__main__":
+ print("=" * 60)
+ print("AIGVDet Training")
+ print("=" * 60)
+
+ print("\nInitializing...")
val_cfg = get_val_cfg(cfg, split="val", copy=True)
cfg.dataset_root = os.path.join(cfg.dataset_root, "train")
+
+ print("Loading training data...")
data_loader = create_dataloader(cfg)
dataset_size = len(data_loader)
+ print(f"✓ Loaded {dataset_size * cfg.batch_size} training images")
log = Logger()
log.open(cfg.logs_path, mode="a")
log.write("Num of training images = %d\n" % (dataset_size * cfg.batch_size))
log.write("Config:\n" + str(cfg.to_dict()) + "\n")
+ print("Setting up TensorBoard...")
train_writer = SummaryWriter(os.path.join(cfg.exp_dir, "train"))
val_writer = SummaryWriter(os.path.join(cfg.exp_dir, "val"))
-
+ print(f"✓ Logs will be saved to: {cfg.exp_dir}")
+
+ # Initialize wandb
+ print("\nInitializing wandb...")
+ wandb.init(
+ project="aigvdet-training",
+ name=cfg.exp_name,
+ config=cfg.to_dict(),
+ dir=cfg.exp_dir,
+ resume="allow" if cfg.continue_train else None
+ )
+ print(f"✓ wandb tracking enabled: {wandb.run.url}")
+
+ print("\nInitializing model...")
trainer = Trainer(cfg)
early_stopping = EarlyStopping(patience=cfg.earlystop_epoch, delta=-0.001, verbose=True)
+ print(f"✓ Model ready (Architecture: {cfg.arch})")
+
+ # Track best model accuracy
+ best_acc = 0.0
+
+ print("\n" + "=" * 60)
+ print(f"Starting training for {cfg.nepoch} epochs")
+ print("=" * 60 + "\n")
+
for epoch in range(cfg.nepoch):
+ print(f"\n{'='*60}")
+ print(f"Epoch {epoch+1}/{cfg.nepoch}")
+ print(f"{'='*60}")
epoch_start_time = time.time()
iter_data_time = time.time()
epoch_iter = 0
- for data in tqdm(data_loader, dynamic_ncols=True):
+ for data in tqdm(data_loader, desc=f"Training Epoch {epoch+1}", unit="batch"):
trainer.total_steps += 1
epoch_iter += cfg.batch_size
@@ -47,8 +94,10 @@
# if trainer.total_steps % cfg.loss_freq == 0:
# log.write(f"Train loss: {trainer.loss} at step: {trainer.total_steps}\n")
train_writer.add_scalar("loss", trainer.loss, trainer.total_steps)
+ wandb.log({"train/loss": trainer.loss, "train/step": trainer.total_steps})
if trainer.total_steps % cfg.save_latest_freq == 0:
+ print(f"💾 Saving checkpoint (epoch {epoch+1}, step {trainer.total_steps})")
log.write(
"saving the latest model %s (epoch %d, model.total_steps %d)\n"
% (cfg.exp_name, epoch, trainer.total_steps)
@@ -56,11 +105,36 @@
trainer.save_networks("latest")
if epoch % cfg.save_epoch_freq == 0:
+ print(f"💾 Saving epoch checkpoint {epoch+1}")
+ log.write("saving the model at the end of epoch %d, iters %d\n" % (epoch, trainer.total_steps))
+ trainer.save_networks("latest")
+ trainer.save_networks(epoch)
+
+ # Validation
+ print("\n🔍 Running validation...")
+ trainer.eval()
+ val_results = validate(trainer.model, val_cfg)
+ val_writer.add_scalar("AP", val_results["AP"], trainer.total_steps)
+ val_writer.add_scalar("ACC", val_results["ACC"], trainer.total_steps)
+ # add
+ val_writer.add_scalar("AUC", val_results["AUC"], trainer.total_steps)
+ val_writer.add_scalar("TPR", val_results["TPR"], trainer.total_steps)
+ val_writer.add_scalar("TNR", val_results["TNR"], trainer.total_steps)
+
+ # Log validation metrics to wandb
+ "saving the latest model %s (epoch %d, model.total_steps %d)\n"
+ % (cfg.exp_name, epoch, trainer.total_steps)
+ )
+ trainer.save_networks("latest")
+
+ if epoch % cfg.save_epoch_freq == 0:
+ print(f"💾 Saving epoch checkpoint {epoch+1}")
log.write("saving the model at the end of epoch %d, iters %d\n" % (epoch, trainer.total_steps))
trainer.save_networks("latest")
trainer.save_networks(epoch)
# Validation
+ print("\n🔍 Running validation...")
trainer.eval()
val_results = validate(trainer.model, val_cfg)
val_writer.add_scalar("AP", val_results["AP"], trainer.total_steps)
@@ -70,18 +144,73 @@
val_writer.add_scalar("TPR", val_results["TPR"], trainer.total_steps)
val_writer.add_scalar("TNR", val_results["TNR"], trainer.total_steps)
+ # Log validation metrics to wandb
+ wandb.log({
+ "val/AP": val_results["AP"],
+ "val/ACC": val_results["ACC"],
+ "val/AUC": val_results["AUC"],
+ "val/TPR": val_results["TPR"],
+ "val/TNR": val_results["TNR"],
+ "val/best_ACC": max(best_acc, val_results["ACC"]),
+ "epoch": epoch
+ })
+
+ print(f"✓ Validation Results - AP: {val_results['AP']:.4f} | ACC: {val_results['ACC']:.4f} | AUC: {val_results['AUC']:.4f}")
log.write(f"(Val @ epoch {epoch}) AP: {val_results['AP']}; ACC: {val_results['ACC']}\n")
+ # Save best model if accuracy improves
+ if val_results['ACC'] > best_acc:
+ print(f"⭐ New best model! (ACC: {best_acc:.4f} -> {val_results['ACC']:.4f})")
+ # Calculate improvement
+ improvement = val_results['ACC'] - best_acc
+ best_acc = val_results['ACC']
+
+ # Explicitly update summary for the table with ALL metrics from this best epoch
+ wandb.run.summary["best_epoch"] = epoch
+ wandb.run.summary["best_AUC"] = val_results["AUC"]
+ wandb.run.summary["best_AP"] = val_results["AP"]
+ wandb.run.summary["best_TPR"] = val_results["TPR"]
+ wandb.run.summary["best_TNR"] = val_results["TNR"]
+
+ # Force update the specific columns you want in the table
+ wandb.run.summary["best_model/score"] = best_acc
+ wandb.run.summary["best_model/improvement"] = improvement
+
+ trainer.save_networks("best")
+
+ best_model_path = os.path.join(cfg.ckpt_dir, "model_epoch_best.pth")
+ if os.path.exists(best_model_path):
+ # 1. Log as Artifact
+ artifact = wandb.Artifact(
+ name=f"{cfg.exp_name}_best_model",
+ type="model",
+ description=f"Best model checkpoint at epoch {epoch} (ACC: {best_acc:.4f})"
+ )
+ artifact.add_file(best_model_path)
+ wandb.log_artifact(artifact)
+
+ # 2. Force upload to Files tab immediately
+ wandb.save(best_model_path, base_path=cfg.root_dir, policy="now")
+ print(f"✓ Uploaded best model to WandB Files: {best_model_path}")
+
if cfg.earlystop:
early_stopping(val_results["ACC"], trainer)
if early_stopping.early_stop:
if trainer.adjust_learning_rate():
+ print("📉 Learning rate dropped by 10, continuing training...")
log.write("Learning rate dropped by 10, continue training...\n")
+ wandb.log({"early_stopping/lr_dropped": True, "epoch": epoch})
early_stopping = EarlyStopping(patience=cfg.earlystop_epoch, delta=-0.002, verbose=True)
else:
+ print("\n⏹️ Early stopping triggered")
log.write("Early stopping.\n")
+ wandb.log({"early_stopping/triggered": True, "epoch": epoch})
break
if cfg.warmup:
# print(trainer.scheduler.get_lr()[0])
trainer.scheduler.step()
trainer.train()
+
+ # Finish wandb run
+ print("\n✓ Training complete! Finalizing W&B...")
+ wandb.finish()
diff --git a/train.sh b/train.sh
deleted file mode 100644
index d8dd0b3..0000000
--- a/train.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-EXP_NAME="moonvalley_vos2_crop"
-DATASETS="moonvalley_vos2_crop"
-DATASETS_TEST="moonvalley_vos2_crop"
-python train.py --gpus 0 --exp_name $EXP_NAME datasets $DATASETS datasets_test $DATASETS_TEST
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..afddae4
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1045 @@
+version = 1
+revision = 3
+requires-python = "==3.11.*"
+resolution-markers = [
+ "sys_platform == 'darwin'",
+ "platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
+]
+
+[[package]]
+name = "absl-py"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" },
+]
+
+[[package]]
+name = "aigvdet"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "blobfile" },
+ { name = "einops" },
+ { name = "imageio" },
+ { name = "ipympl" },
+ { name = "matplotlib" },
+ { name = "numpy" },
+ { name = "opencv-python" },
+ { name = "pandas" },
+ { name = "pip" },
+ { name = "scikit-learn" },
+ { name = "tensorboard" },
+ { name = "tensorboardx" },
+ { name = "torch" },
+ { name = "torchvision" },
+ { name = "tqdm" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "hatchling" },
+ { name = "setuptools" },
+ { name = "wheel" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "blobfile", specifier = ">=1.0.5" },
+ { name = "einops" },
+ { name = "imageio" },
+ { name = "ipympl" },
+ { name = "matplotlib" },
+ { name = "numpy", specifier = "<2.0" },
+ { name = "opencv-python" },
+ { name = "pandas" },
+ { name = "pip" },
+ { name = "scikit-learn" },
+ { name = "tensorboard" },
+ { name = "tensorboardx" },
+ { name = "torch", specifier = "==2.0.0+cu117", index = "https://download.pytorch.org/whl/cu117" },
+ { name = "torchvision", specifier = "==0.15.1+cu117", index = "https://download.pytorch.org/whl/cu117" },
+ { name = "tqdm" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "hatchling" },
+ { name = "setuptools" },
+ { name = "wheel" },
+]
+
+[[package]]
+name = "asttokens"
+version = "3.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
+]
+
+[[package]]
+name = "blobfile"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "lxml" },
+ { name = "pycryptodomex" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f0/6d/2e7567da75ddbb24fe979f52284b708da349d67a41042635af36071a5a6b/blobfile-3.1.0.tar.gz", hash = "sha256:d45b6b1fa3b0920732314c23ddbdb4f494ca12f787c2b6eb6bba6faa51382671", size = 77229, upload-time = "2025-09-06T00:36:15.583Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/a7/51af11120d75af2828f8eede0b13a4caff650d708ac50e62d000aefe1ffb/blobfile-3.1.0-py3-none-any.whl", hash = "sha256:2b4c5e766ebb7dfa20e4990cf6ec3d2106bdc91d632fb9377f170a234c5a5c6a", size = 75741, upload-time = "2025-09-06T00:36:14.11Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2022.12.7"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "2.1.1"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" },
+]
+
+[[package]]
+name = "cmake"
+version = "3.25.0"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/cmake-3.25.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7fd744a90e4d804ff77ac50d3570009911fbfdad29c59fc93d2a82faaeb371" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
+]
+
+[[package]]
+name = "comm"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
+ { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
+ { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "decorator"
+version = "5.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
+]
+
+[[package]]
+name = "einops"
+version = "0.8.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805, upload-time = "2025-02-09T03:17:00.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359, upload-time = "2025-02-09T03:17:01.998Z" },
+]
+
+[[package]]
+name = "executing"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.19.1"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/filelock-3.19.1-py3-none-any.whl" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.60.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" },
+ { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
+]
+
+[[package]]
+name = "grpcio"
+version = "1.76.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" },
+ { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" },
+ { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" },
+ { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" },
+]
+
+[[package]]
+name = "hatchling"
+version = "1.25.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "pluggy" },
+ { name = "trove-classifiers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/51/8a4a67a8174ce59cf49e816e38e9502900aea9b4af672d0127df8e10d3b0/hatchling-1.25.0.tar.gz", hash = "sha256:7064631a512610b52250a4d3ff1bd81551d6d1431c4eb7b72e734df6c74f4262", size = 64632, upload-time = "2024-06-22T17:27:01.766Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/8b/90e80904fdc24ce33f6fc6f35ebd2232fe731a8528a22008458cf197bc4d/hatchling-1.25.0-py3-none-any.whl", hash = "sha256:b47948e45d4d973034584dd4cb39c14b6a70227cf287ab7ec0ad7983408a882c", size = 84077, upload-time = "2024-06-22T17:26:59.671Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.4"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" },
+]
+
+[[package]]
+name = "imageio"
+version = "2.37.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "pillow" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" },
+]
+
+[[package]]
+name = "ipympl"
+version = "0.9.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ipython" },
+ { name = "ipywidgets" },
+ { name = "matplotlib" },
+ { name = "numpy" },
+ { name = "pillow" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/8c/f9e60abf409cef8234e66e69ce3fe263f1236b285f9105ea125e4660b77a/ipympl-0.9.8.tar.gz", hash = "sha256:6d7230d518384521093f3854f7db89d069dcd9c28a935b371e9c9f126354dee1", size = 58483988, upload-time = "2025-10-09T14:20:07.741Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/6e/9148bfed8ca535e4c61ce7843327c76ec7c63c40e33848ec03aa844a26af/ipympl-0.9.8-py3-none-any.whl", hash = "sha256:4a03612f77d92c9e2160c9e0d2a80b277e30387126399088f780dba9622247be", size = 515832, upload-time = "2025-10-09T14:20:05.39Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "9.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "decorator" },
+ { name = "ipython-pygments-lexers" },
+ { name = "jedi" },
+ { name = "matplotlib-inline" },
+ { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit" },
+ { name = "pygments" },
+ { name = "stack-data" },
+ { name = "traitlets" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f", size = 618911, upload-time = "2025-11-05T12:18:52.484Z" },
+]
+
+[[package]]
+name = "ipython-pygments-lexers"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
+]
+
+[[package]]
+name = "ipywidgets"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "comm" },
+ { name = "ipython" },
+ { name = "jupyterlab-widgets" },
+ { name = "traitlets" },
+ { name = "widgetsnbextension" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" },
+]
+
+[[package]]
+name = "jedi"
+version = "0.19.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "parso" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+dependencies = [
+ { name = "markupsafe" },
+]
+wheels = [
+ { url = "https://download.pytorch.org/whl/jinja2-3.1.6-py3-none-any.whl" },
+]
+
+[[package]]
+name = "joblib"
+version = "1.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" },
+]
+
+[[package]]
+name = "jupyterlab-widgets"
+version = "3.0.16"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" },
+ { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" },
+ { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" },
+ { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" },
+ { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" },
+ { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" },
+]
+
+[[package]]
+name = "lit"
+version = "15.0.7"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+sdist = { url = "https://download.pytorch.org/whl/lit-15.0.7.tar.gz", hash = "sha256:ed08ac55afe714a193653df293ae8a6ee6c45d6fb11eeca72ce347d99b88ecc8" }
+
+[[package]]
+name = "lxml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" },
+ { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" },
+ { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" },
+ { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" },
+ { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" },
+ { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" },
+ { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" },
+ { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" },
+]
+
+[[package]]
+name = "markdown"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.5"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+sdist = { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f" },
+ { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2" },
+ { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced" },
+ { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5" },
+ { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c" },
+ { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" },
+ { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" },
+ { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" },
+ { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" },
+ { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" },
+]
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.5"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/networkx-3.5-py3-none-any.whl" },
+]
+
+[[package]]
+name = "numpy"
+version = "1.26.3"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374" },
+ { url = "https://download.pytorch.org/whl/numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6" },
+ { url = "https://download.pytorch.org/whl/numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2" },
+ { url = "https://download.pytorch.org/whl/numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda" },
+ { url = "https://download.pytorch.org/whl/numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4" },
+]
+
+[[package]]
+name = "opencv-python"
+version = "4.11.0.86"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "24.1"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
+ { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
+]
+
+[[package]]
+name = "parso"
+version = "0.8.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+]
+
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "11.3.0"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl" },
+ { url = "https://download.pytorch.org/whl/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl" },
+ { url = "https://download.pytorch.org/whl/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" },
+ { url = "https://download.pytorch.org/whl/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" },
+ { url = "https://download.pytorch.org/whl/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl" },
+ { url = "https://download.pytorch.org/whl/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" },
+ { url = "https://download.pytorch.org/whl/pillow-11.3.0-cp311-cp311-win_amd64.whl" },
+ { url = "https://download.pytorch.org/whl/pillow-11.3.0-cp311-cp311-win_arm64.whl" },
+]
+
+[[package]]
+name = "pip"
+version = "25.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.52"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.33.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" },
+]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
+]
+
+[[package]]
+name = "pycryptodomex"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" },
+ { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" },
+ { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" },
+ { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.2.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.28.1"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+wheels = [
+ { url = "https://download.pytorch.org/whl/requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "joblib" },
+ { name = "numpy" },
+ { name = "scipy" },
+ { name = "threadpoolctl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.16.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" },
+ { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" },
+ { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" },
+ { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "70.2.0"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asttokens" },
+ { name = "executing" },
+ { name = "pure-eval" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+dependencies = [
+ { name = "mpmath" },
+]
+wheels = [
+ { url = "https://download.pytorch.org/whl/sympy-1.14.0-py3-none-any.whl" },
+]
+
+[[package]]
+name = "tensorboard"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "absl-py" },
+ { name = "grpcio" },
+ { name = "markdown" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "protobuf" },
+ { name = "setuptools" },
+ { name = "tensorboard-data-server" },
+ { name = "werkzeug" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" },
+]
+
+[[package]]
+name = "tensorboard-data-server"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" },
+ { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" },
+]
+
+[[package]]
+name = "tensorboardx"
+version = "2.6.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2b/c5/d4cc6e293fb837aaf9f76dd7745476aeba8ef7ef5146c3b3f9ee375fe7a5/tensorboardx-2.6.4.tar.gz", hash = "sha256:b163ccb7798b31100b9f5fa4d6bc22dad362d7065c2f24b51e50731adde86828", size = 4769801, upload-time = "2025-06-10T22:37:07.419Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/1d/b5d63f1a6b824282b57f7b581810d20b7a28ca951f2d5b59f1eb0782c12b/tensorboardx-2.6.4-py3-none-any.whl", hash = "sha256:5970cf3a1f0a6a6e8b180ccf46f3fe832b8a25a70b86e5a237048a7c0beb18e2", size = 87201, upload-time = "2025-06-10T22:37:05.44Z" },
+]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.0.0+cu117"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+dependencies = [
+ { name = "filelock" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "sympy" },
+ { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "typing-extensions" },
+]
+wheels = [
+ { url = "https://download.pytorch.org/whl/cu117/torch-2.0.0%2Bcu117-cp311-cp311-linux_x86_64.whl", hash = "sha256:b078675648025f1dae1cdc8955f975f5ca81167809e9662b1481e456171ebfb9" },
+ { url = "https://download.pytorch.org/whl/cu117/torch-2.0.0%2Bcu117-cp311-cp311-win_amd64.whl", hash = "sha256:f0b525686f25c30e1de87d0fbdcd0b373f4c70a0f72bd854389e601a52fdc5e5" },
+]
+
+[[package]]
+name = "torchvision"
+version = "0.15.1+cu117"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+dependencies = [
+ { name = "numpy" },
+ { name = "pillow" },
+ { name = "requests" },
+ { name = "torch" },
+]
+wheels = [
+ { url = "https://download.pytorch.org/whl/cu117/torchvision-0.15.1%2Bcu117-cp311-cp311-linux_x86_64.whl", hash = "sha256:67ff110078913bf684893c4a13d9415f11f6908f40f33fa6626726913196b279" },
+ { url = "https://download.pytorch.org/whl/cu117/torchvision-0.15.1%2Bcu117-cp311-cp311-win_amd64.whl", hash = "sha256:6d82ede144cabe85f21814c723b3a666d427de9566056701863663ea0f2ad7d9" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.66.5"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+wheels = [
+ { url = "https://download.pytorch.org/whl/tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd" },
+]
+
+[[package]]
+name = "traitlets"
+version = "5.14.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
+]
+
+[[package]]
+name = "triton"
+version = "2.0.0"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+dependencies = [
+ { name = "cmake" },
+ { name = "filelock" },
+ { name = "lit" },
+ { name = "torch" },
+]
+wheels = [
+ { url = "https://download.pytorch.org/whl/triton-2.0.0-1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:226941c7b8595219ddef59a1fdb821e8c744289a132415ddd584facedeb475b1" },
+ { url = "https://download.pytorch.org/whl/triton-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4b99ca3c6844066e516658541d876c28a5f6e3a852286bbc97ad57134827fd" },
+]
+
+[[package]]
+name = "trove-classifiers"
+version = "2025.11.14.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bf/a9/880cccf76af9e7b322112f52e4e2dbb3534cbe671197b8f443a42189dfc7/trove_classifiers-2025.11.14.15.tar.gz", hash = "sha256:6b60f49d40bbd895bc61d8dc414fc2f2286d70eb72ed23548db8cf94f62804ca", size = 16995, upload-time = "2025-11-14T15:23:13.78Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/f6/73c4aa003d1237ee9bea8a46f49dc38c45dfe95af4f0da7e60678d388011/trove_classifiers-2025.11.14.15-py3-none-any.whl", hash = "sha256:d1dac259c1e908939862e3331177931c6df0a37af2c1a8debcc603d9115fcdd9", size = 14191, upload-time = "2025-11-14T15:23:12.467Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/typing_extensions-4.15.0-py3-none-any.whl" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "1.26.13"
+source = { registry = "https://download.pytorch.org/whl/cu117" }
+wheels = [
+ { url = "https://download.pytorch.org/whl/urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc" },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
+]
+
+[[package]]
+name = "wheel"
+version = "0.45.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" },
+]
+
+[[package]]
+name = "widgetsnbextension"
+version = "4.0.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" },
+]