Commit ·
c53fe07
0
Parent(s):
Deploy: integrate visual style analysis, remove filename scoring layer
Browse files- .dockerignore +10 -0
- .gitignore +146 -0
- Dockerfile +52 -0
- PROJECT_OVERVIEW.md +152 -0
- README.md +116 -0
- backend/__init__.py +0 -0
- backend/detectors/__init__.py +0 -0
- backend/detectors/filename_detector.py +117 -0
- backend/detectors/image_detector.py +523 -0
- backend/detectors/metadata_detector.py +163 -0
- backend/detectors/scoring_engine.py +132 -0
- backend/detectors/style_detector.py +66 -0
- backend/main.py +77 -0
- frontend/index.html +494 -0
- frontend/static/css/style.css +693 -0
- frontend/static/js/app.js +397 -0
- public/.gitkeep +0 -0
- requirements.txt +10 -0
- run.py +8 -0
.dockerignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.git/
|
| 5 |
+
.gemini/
|
| 6 |
+
database/
|
| 7 |
+
frontend/static/uploads/
|
| 8 |
+
*.db
|
| 9 |
+
.DS_Store
|
| 10 |
+
.gitignore
|
.gitignore
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
|
| 7 |
+
# C extensions
|
| 8 |
+
*.so
|
| 9 |
+
|
| 10 |
+
# Distribution / packaging
|
| 11 |
+
.Python
|
| 12 |
+
build/
|
| 13 |
+
develop-eggs/
|
| 14 |
+
dist/
|
| 15 |
+
downloads/
|
| 16 |
+
eggs/
|
| 17 |
+
.eggs/
|
| 18 |
+
lib/
|
| 19 |
+
lib64/
|
| 20 |
+
parts/
|
| 21 |
+
sdist/
|
| 22 |
+
var/
|
| 23 |
+
wheels/
|
| 24 |
+
share/python-wheels/
|
| 25 |
+
*.egg-info/
|
| 26 |
+
.installed.cfg
|
| 27 |
+
*.egg
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script run during development
|
| 31 |
+
*.manifest
|
| 32 |
+
*.spec
|
| 33 |
+
|
| 34 |
+
# Installer logs
|
| 35 |
+
pip-log.txt
|
| 36 |
+
pip-delete-this-directory.txt
|
| 37 |
+
|
| 38 |
+
# Unit test / coverage reports
|
| 39 |
+
htmlcov/
|
| 40 |
+
.tox/
|
| 41 |
+
.nosenv/
|
| 42 |
+
.pytest_cache/
|
| 43 |
+
.mypy_cache/
|
| 44 |
+
.ruff_cache/
|
| 45 |
+
|
| 46 |
+
# Translations
|
| 47 |
+
*.mo
|
| 48 |
+
*.gmo
|
| 49 |
+
|
| 50 |
+
# Django stuff:
|
| 51 |
+
*.log
|
| 52 |
+
local_settings.py
|
| 53 |
+
db.sqlite3
|
| 54 |
+
db.sqlite3-journal
|
| 55 |
+
|
| 56 |
+
# Sphinx documentation
|
| 57 |
+
docs/_build/
|
| 58 |
+
|
| 59 |
+
# PyBuilder
|
| 60 |
+
.pybuilder/
|
| 61 |
+
target/
|
| 62 |
+
|
| 63 |
+
# Jupyter Notebook
|
| 64 |
+
.ipynb_checkpoints
|
| 65 |
+
|
| 66 |
+
# IPython
|
| 67 |
+
profile_default/
|
| 68 |
+
ipython_config.py
|
| 69 |
+
|
| 70 |
+
# Virtual Environments
|
| 71 |
+
.venv/
|
| 72 |
+
venv/
|
| 73 |
+
ENV/
|
| 74 |
+
env/
|
| 75 |
+
ENV/
|
| 76 |
+
ENV/local/
|
| 77 |
+
ENV/lib/
|
| 78 |
+
|
| 79 |
+
# Poetry
|
| 80 |
+
.poetry/
|
| 81 |
+
|
| 82 |
+
# Pipenv
|
| 83 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 84 |
+
# However, if you're executing a microservice that you deploy, you should commit Pipfile.lock.
|
| 85 |
+
# Pipfile.lock
|
| 86 |
+
|
| 87 |
+
# PEP 582; used by e.g. pdm
|
| 88 |
+
__pypackages__/
|
| 89 |
+
|
| 90 |
+
# Celery stuff
|
| 91 |
+
celerybeat-schedule
|
| 92 |
+
celerybeat.pid
|
| 93 |
+
|
| 94 |
+
# SageMath parsed files
|
| 95 |
+
*.sage.py
|
| 96 |
+
|
| 97 |
+
# Environments
|
| 98 |
+
.env
|
| 99 |
+
.env.local
|
| 100 |
+
.env.development.local
|
| 101 |
+
.env.test.local
|
| 102 |
+
.env.production.local
|
| 103 |
+
.env.production
|
| 104 |
+
.env.development
|
| 105 |
+
.env.staging
|
| 106 |
+
|
| 107 |
+
# Spyder project settings
|
| 108 |
+
.spyderproject
|
| 109 |
+
.spyder-py3
|
| 110 |
+
|
| 111 |
+
# Rope project settings
|
| 112 |
+
.ropeproject
|
| 113 |
+
|
| 114 |
+
# mkdocs documentation
|
| 115 |
+
/site
|
| 116 |
+
|
| 117 |
+
# mypy
|
| 118 |
+
.mypy_cache/
|
| 119 |
+
.dmypy.json
|
| 120 |
+
dmypy.json
|
| 121 |
+
|
| 122 |
+
# Pyre type checker
|
| 123 |
+
.pyre/
|
| 124 |
+
|
| 125 |
+
# pytype static analyzer
|
| 126 |
+
.pytype/
|
| 127 |
+
|
| 128 |
+
# Cython debug symbols
|
| 129 |
+
cython_debug/
|
| 130 |
+
|
| 131 |
+
# OS metadata
|
| 132 |
+
.DS_Store
|
| 133 |
+
Thumbs.db
|
| 134 |
+
|
| 135 |
+
# IDEs
|
| 136 |
+
.vscode/
|
| 137 |
+
.idea/
|
| 138 |
+
|
| 139 |
+
# Custom Exclusions for ImgAuth AI
|
| 140 |
+
ImgAuthAI.zip
|
| 141 |
+
node_modules/
|
| 142 |
+
dist/
|
| 143 |
+
out/
|
| 144 |
+
|
| 145 |
+
# Generated heatmap uploads (runtime artifacts, not source files)
|
| 146 |
+
frontend/static/uploads/
|
Dockerfile
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a slim Python 3.11 base image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies needed for OpenCV, PyTorch, and general builds
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
build-essential \
|
| 7 |
+
libgl1 \
|
| 8 |
+
libglib2.0-0 \
|
| 9 |
+
git \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Set up user and working directory (required for HF Spaces)
|
| 13 |
+
# HF Spaces runs containers with user UID 1000
|
| 14 |
+
RUN useradd -m -u 1000 user
|
| 15 |
+
WORKDIR /code
|
| 16 |
+
|
| 17 |
+
# Set Hugging Face cache directory to a writeable location
|
| 18 |
+
ENV HF_HOME=/code/.cache/huggingface
|
| 19 |
+
RUN mkdir -p /code/.cache/huggingface && chown -R user:user /code
|
| 20 |
+
|
| 21 |
+
# Copy requirements file first to leverage Docker cache
|
| 22 |
+
COPY --chown=user:user requirements.txt /code/requirements.txt
|
| 23 |
+
|
| 24 |
+
# Install dependencies
|
| 25 |
+
# Optimize PyTorch installation to use CPU-only binaries to save space
|
| 26 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 27 |
+
pip install --no-cache-dir torch==2.3.0 torchvision==0.18.0 --index-url https://download.pytorch.org/whl/cpu && \
|
| 28 |
+
pip install --no-cache-dir -r requirements.txt
|
| 29 |
+
|
| 30 |
+
# Copy the rest of the application files
|
| 31 |
+
COPY --chown=user:user . /code
|
| 32 |
+
|
| 33 |
+
# Pre-download Hugging Face models during build to speed up container startup.
|
| 34 |
+
# This avoids container startup timeouts and lazy-loading delays.
|
| 35 |
+
RUN python -c "\
|
| 36 |
+
from transformers import pipeline; \
|
| 37 |
+
pipeline('image-classification', model='umm-maybe/AI-image-detector'); \
|
| 38 |
+
pipeline('image-classification', model='dima806/ai_vs_real_image_detection'); \
|
| 39 |
+
pipeline('image-classification', model='Organika/sdxl-detector') \
|
| 40 |
+
"
|
| 41 |
+
|
| 42 |
+
# Set permissions
|
| 43 |
+
RUN chmod -R 777 /code
|
| 44 |
+
|
| 45 |
+
# Switch to non-root user
|
| 46 |
+
USER user
|
| 47 |
+
|
| 48 |
+
# Expose port 7860 (Hugging Face Spaces default)
|
| 49 |
+
EXPOSE 7860
|
| 50 |
+
|
| 51 |
+
# Run uvicorn server mapping to port 7860
|
| 52 |
+
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
PROJECT_OVERVIEW.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ImgAuth AI — Project Overview & Architecture Guide
|
| 2 |
+
|
| 3 |
+
Welcome to the official documentation and technical overview of **ImgAuth AI**. This guide is designed to explain how our AI Image Authenticity Detector works in simple language, breaking down the backend analysis pipelines, the deep learning models, the forensic heuristics, the explainability heatmaps, and the frontend design.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 📌 Project Goal & Philosophy
|
| 8 |
+
|
| 9 |
+
**ImgAuth AI** is a major college project developed by **Team VisionGuard** (Vishal, Prince M., Prince D., and Raksha).
|
| 10 |
+
|
| 11 |
+
### The Core Philosophy: *“Simple on the surface, powerful underneath.”*
|
| 12 |
+
* **For Everyday Users**: It works like a clean, minimal SaaS tool. You drag and drop an image, click a button, and immediately get a clear binary answer: **Likely AI-Generated** or **Likely Authentic**, accompanied by a confidence percentage and three simple reasons.
|
| 13 |
+
* **For Investigators & Researchers**: Under a collapsed **"Show Technical Analysis"** drawer, the app reveals detailed forensic logs, individual model scores, neural network attentions, and math-heavy metrics (such as Kurtosis, Spectral ratios, and Compression ghosts).
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 🔄 End-to-End Image Processing Workflow
|
| 18 |
+
|
| 19 |
+
When a user uploads an image, the system processes it in six main steps:
|
| 20 |
+
|
| 21 |
+
```mermaid
|
| 22 |
+
graph TD
|
| 23 |
+
A[User Uploads Image] --> B[Step 1: Filename Scan]
|
| 24 |
+
A --> C[Step 2: Metadata Inspection]
|
| 25 |
+
A --> D[Step 3: Deep Learning Models]
|
| 26 |
+
A --> E[Step 4: Forensic Heuristics]
|
| 27 |
+
B & C & D & E --> F[Step 5: Scoring Engine]
|
| 28 |
+
F --> G[Step 6: Explainability Heatmaps]
|
| 29 |
+
G --> H[Frontend Results Displayed]
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
1. **Upload & Reading**: The frontend sends the image to the FastAPI backend as a binary stream. The backend loads it using **Pillow** (PIL) and **OpenCV** in memory (without saving it to disk for privacy).
|
| 33 |
+
2. **Parallel Scans**: The image is fed simultaneously into four analysis layers:
|
| 34 |
+
- Filename clues
|
| 35 |
+
- Metadata headers
|
| 36 |
+
- 3 Deep Learning neural networks
|
| 37 |
+
- 5 Digital forensic algorithms
|
| 38 |
+
3. **Consolidation**: The **Scoring Engine** takes the probabilities from all layers, applies a dynamic weighting formula based on the strength of the clues, and calculates a final AI probability score.
|
| 39 |
+
4. **Heatmap Generation**: If the image contains a valid vision signature, the system runs ViT (Vision Transformer) attention scans to map exactly which parts of the image look suspicious.
|
| 40 |
+
5. **Response**: The server returns a structured JSON containing the verdict, final score, detailed breakdown, heatmaps, and diagnostic logs.
|
| 41 |
+
6. **Smooth Render**: The frontend receives the JSON, updates the visual elements, and smoothly scrolls the user down to the results.
|
| 42 |
+
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
## 🛡️ The Multi-Layer Detection Architecture
|
| 46 |
+
|
| 47 |
+
ImgAuth AI uses a **four-layer defense system** to catch generated media.
|
| 48 |
+
|
| 49 |
+
### Layer 1: Filename Analysis
|
| 50 |
+
Many AI generators assign unique default filenames to their creations. The backend scans the file name against regular expression (regex) patterns:
|
| 51 |
+
* **Midjourney**: Typically uses 4 dash-separated hexadecimal blocks followed by a seed number (e.g., `user_description_abcd1234-ef56-7890-abcd-1234567890ab.png`).
|
| 52 |
+
* **Stable Diffusion / DALL-E / Bing**: Often contain timestamps, prompt keywords, or specific prefix indicators (e.g., `DALL·E 2026-...`, `BingCreator_...`).
|
| 53 |
+
* **Why it matters**: If a match is found, it is a near-guarantee (92% weight) that the image is AI-generated, bypassing heavy computation or helping confirm suspect classifications.
|
| 54 |
+
|
| 55 |
+
### Layer 2: Metadata Analysis
|
| 56 |
+
Digital cameras embed metadata (EXIF data) inside image files containing details like camera model, exposure, GPS coordinates, and creation software. AI generators either:
|
| 57 |
+
* Strip all metadata completely (leaving a blank EXIF profile).
|
| 58 |
+
* Write their generator signature (e.g., software tags like `"Adobe Firefly"`, `"DALL-E"`, or `"Stable Diffusion"`).
|
| 59 |
+
* **How it works**: The backend parses the raw file bytes looking for telltale creator software tags or signs of metadata stripping.
|
| 60 |
+
|
| 61 |
+
### Layer 3: Deep Learning Ensemble
|
| 62 |
+
We feed the image into three separate state-of-the-art neural networks hosted on Hugging Face. Combining their opinions avoids the bias of a single model:
|
| 63 |
+
|
| 64 |
+
| Model ID | Name | Architecture Type | What It Focuses On | Weight |
|
| 65 |
+
| :--- | :--- | :--- | :--- | :--- |
|
| 66 |
+
| `umm-maybe/AI-image-detector` | **umm-maybe ViT** | Vision Transformer (ViT) | Global semantic consistency (does the overall scene make physical sense?) | 25% |
|
| 67 |
+
| `dima806/ai_vs_real_image_detection` | **dima806 CNN** | Convolutional Neural Network (CNN) | Local textures and pixel anomalies (brush strokes, unnatural blending) | 25% |
|
| 68 |
+
| `Organika/sdxl-detector` | **Organika SDXL** | Specialized CNN | Specific artifacts produced by Stable Diffusion XL generators | 25% |
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
### Layer 4: Advanced Forensic Heuristics
|
| 73 |
+
Deep learning models can be fooled by filters or resizing. To counter this, ImgAuth AI uses **five math-based forensic checks** that look at the physics of pixels:
|
| 74 |
+
|
| 75 |
+
#### 1. Noise Kurtosis Analysis (Weight: 8%)
|
| 76 |
+
* **Concept**: Real cameras capture random high-frequency thermal noise in pixels. When AI generates an image, it smooths out or over-complicates this noise.
|
| 77 |
+
* **How it works**: We apply a **Laplacian filter** to isolate high-frequency details (noise), flatten the pixels into a 1D list, and calculate its **kurtosis** (a statistical measure of how "peaky" or "flat" the distribution is).
|
| 78 |
+
* *High Kurtosis (> 1.5)*: Leptokurtic curve. Means noise is natural and random (Likely Authentic).
|
| 79 |
+
* *Low/Negative Kurtosis (< 0)*: Platykurtic curve. Means noise is highly uniform or artificially smooth (Likely AI-Generated).
|
| 80 |
+
|
| 81 |
+
#### 2. Deep Feature Inconsistency (DFI) (Weight: 7%)
|
| 82 |
+
* **Concept**: Generative models create images patch-by-patch, which often leads to subtle boundary mismatches between different areas of the image.
|
| 83 |
+
* **How it works**: We pass the image through the Vision Transformer (ViT) and extract the hidden features of the very last neural layer. We calculate the mathematical similarity (Cosine Similarity) between every single patch and the average of all patches.
|
| 84 |
+
* If the variance of these similarities is high, it shows that different parts of the image have inconsistent mathematical characteristics—a classic indicator of patch-based AI generation.
|
| 85 |
+
|
| 86 |
+
#### 3. FFT Spectral Analysis (Weight: 5%)
|
| 87 |
+
* **Concept**: When generating an image, AI algorithms upscale pixel grids, which leaves behind faint, repeating grid-like patterns called "periodic artifacts" (invisible to the human eye).
|
| 88 |
+
* **How it works**: We perform a **Fast Fourier Transform (FFT)** to convert the image from pixels (spatial domain) into frequencies (spectral domain). We look for abnormal high-frequency spikes.
|
| 89 |
+
* A high ratio of periodic spikes in the outer rings of the frequency map indicates unnatural, repeating grid artifacts common in AI generation.
|
| 90 |
+
|
| 91 |
+
#### 4. Color Histogram Analysis (Weight: 3%)
|
| 92 |
+
* **Concept**: Real photographs have smooth, gradual transitions of colors due to natural lighting. AI-generated images often have abrupt color steps or overly simplified color palettes.
|
| 93 |
+
* **How it works**: We plot the color histograms (Red, Green, Blue channels) and calculate the mathematical **roughness** (standard deviation of the differences between consecutive histogram bars).
|
| 94 |
+
* *Rough/Jaggered histograms*: Natural transitions (Likely Authentic).
|
| 95 |
+
* *Extremely smooth histograms*: Artificially simplified color distributions (Likely AI-Generated).
|
| 96 |
+
|
| 97 |
+
#### 5. JPEG Ghost Analysis (Weight: 2%)
|
| 98 |
+
* **Concept**: When you edit or save an image, it undergoes compression. Real images saved multiple times have complex compression "history". AI images are usually generated and saved once, exhibiting highly uniform compression.
|
| 99 |
+
* **How it works**: We compress the image at different quality levels (e.g., 60%, 70%, 80%) and calculate the difference between the re-compressed versions.
|
| 100 |
+
* If the difference (ghost spread) is very low and uniform across the image, it suggests the image was synthetically generated and compressed only once.
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
## 🧮 Scoring Engine & Decision Logic
|
| 105 |
+
|
| 106 |
+
The scoring engine implements **dynamic weighting**. The weights of the layers shift depending on how strong the clues are:
|
| 107 |
+
|
| 108 |
+
1. **Strong Filename Trigger**: If the filename matches a generator pattern, we skip complex weights and apply:
|
| 109 |
+
* **92% Filename** + **4% Metadata** + **4% Models & Forensics**
|
| 110 |
+
* This immediately pushes the AI probability high while incorporating safety margins.
|
| 111 |
+
2. **Medium Clue / No Clue**: If the filename has no obvious markers, the weights distribute:
|
| 112 |
+
* **10% Filename** + **45% Metadata** + **45% Models & Forensics**
|
| 113 |
+
3. **Ensemble Aggregation**: The Deep Learning models and the 5 Forensic Heuristics are aggregated based on their individual weights ($25\% + 25\% + 25\% + 8\% + 7\% + 5\% + 3\% + 2\% = 100\%$).
|
| 114 |
+
4. **Binary Verdict Threshold**:
|
| 115 |
+
* If $\text{Final AI Score} > 50\%$, the verdict is **Likely AI-Generated**.
|
| 116 |
+
* If $\text{Final AI Score} \le 50\%$, the verdict is **Likely Authentic**.
|
| 117 |
+
* *Confidence Label*: Derived from how far the score is from the $50\%$ middle boundary (e.g., $95\%$ score is "Very High Confidence", $55\%$ is "Low Confidence").
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## 📊 What the Frontend Graphs & Heatmaps Show
|
| 122 |
+
|
| 123 |
+
When analysis is complete, two visual graphs (heatmaps) are rendered under the **AI Focus Areas** tab:
|
| 124 |
+
|
| 125 |
+
### 1. AI Attention Areas (ViT Attention Map)
|
| 126 |
+
* **What it is**: A visual representation of where the Vision Transformer's neural layers "looked" the most when making its decision.
|
| 127 |
+
* **How to read it**:
|
| 128 |
+
* **Red/Warm spots**: Areas where the AI model detected suspicious structures, textures, or anomalies (e.g., malformed eyes, weird background blending, or floating pixels).
|
| 129 |
+
* **Blue/Cool spots**: Neutral areas that the model ignored as background or normal textures.
|
| 130 |
+
* **Value**: Gives the user a visual explanation of *why* the models classified the image as AI-generated.
|
| 131 |
+
|
| 132 |
+
### 2. Pattern Irregularities (DFI Heatmap)
|
| 133 |
+
* **What it is**: A map representing the mathematical inconsistencies (cosine distance variance) between patches of the image.
|
| 134 |
+
* **How to read it**:
|
| 135 |
+
* **Highly fractured/hot spots**: Areas where pixel characteristics deviate sharply from the surrounding regions, highlighting copy-paste edges, localized AI inpainting, or upscaling borders.
|
| 136 |
+
* **Uniform blue fields**: Smooth, physically consistent regions typical of natural, unaltered photography.
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
## 💻 Tech Stack & Component Responsibilities
|
| 141 |
+
|
| 142 |
+
The project is built entirely on a lightweight, performant, and modern Python/JS stack:
|
| 143 |
+
|
| 144 |
+
| Component | Technology | Purpose |
|
| 145 |
+
| :--- | :--- | :--- |
|
| 146 |
+
| **Server/API** | `FastAPI` (Python) | High-performance, asynchronous web server that handles API endpoints. |
|
| 147 |
+
| **Deep Learning** | `PyTorch` & `Transformers` | Loads and runs inference on the Hugging Face vision models. |
|
| 148 |
+
| **Forensics** | `OpenCV`, `NumPy`, `SciPy` | Performs fast matrix math, FFT frequency transforms, noise extraction, and image resizing. |
|
| 149 |
+
| **Structure** | HTML5 | Semantically structures the layout, navigation, cards, and drawers. |
|
| 150 |
+
| **Styling** | CSS3 (Vanilla) | Custom styling utilizing a dark premium theme, glassmorphism card surfaces, purple gradients, responsive media queries, and transition animations. |
|
| 151 |
+
| **Logic** | JavaScript (Vanilla) | Handles drag-and-drop file uploads, coordinates loading states, parses API JSON responses, dynamically updates the DOM, renders base64 heatmaps, and saves/retrieves history using browser `localStorage`. |
|
| 152 |
+
| **Deployment** | `Docker` | Containers the app, installs CPU-optimized PyTorch binaries, and pre-caches the Hugging Face models for easy hosting on Hugging Face Spaces. |
|
README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ImgAuth AI
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# ImgAuth AI — Image Authenticity Detector
|
| 12 |
+
|
| 13 |
+
🛡️ **ImgAuth AI** is a state-of-the-art web application designed to detect AI-generated and manipulated images. Built with a "simple on the surface, powerful underneath" philosophy, it combines deep learning models with advanced digital forensics heuristics to deliver clear, binary verdicts: **Likely AI-Generated** or **Likely Authentic**.
|
| 14 |
+
|
| 15 |
+
Developed as a Major Project by **Team VisionGuard** (student team of 4).
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 🚀 Key Features
|
| 20 |
+
|
| 21 |
+
- **Binary Classification**: Simplified verdicts removing ambiguity ("Likely AI-Generated" or "Likely Authentic").
|
| 22 |
+
- **Deep Learning Ensemble**: Combined predictions from 3 Hugging Face model pipelines:
|
| 23 |
+
- `umm-maybe/AI-image-detector`
|
| 24 |
+
- `dima806/ai_vs_real_image_detection`
|
| 25 |
+
- `Organika/sdxl-detector`
|
| 26 |
+
- **5 Forensic Heuristics**: Multi-layer analysis for technical validation:
|
| 27 |
+
1. *Noise Kurtosis Analysis* (checks high-frequency noise distributions)
|
| 28 |
+
2. *Deep Feature Inconsistency (DFI)* (checks patch-level consistency of Vision Transformer embeddings)
|
| 29 |
+
3. *FFT Spectral Analysis* (identifies periodic artifacts in frequency domain)
|
| 30 |
+
4. *Color Histogram Analysis* (detects synthetic pixel roughness/smoothness)
|
| 31 |
+
5. *JPEG Ghost Analysis* (detects double compression artifacts in JPEG files)
|
| 32 |
+
- **AI Focus Areas (Explainability)**: Visual heatmaps showing ViT Attention Maps and Deep Feature Inconsistencies.
|
| 33 |
+
- **Collapsible Technical Drawer**: Advanced forensic signal logs, weights, and metrics available for researchers, while maintaining a clean, technical-jargon-free interface for everyday users.
|
| 34 |
+
- **Privacy First**: Fully stateless architecture; no images are stored permanently. Scanning history is saved only in local browser storage (`localStorage`).
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## 👥 Meet Team VisionGuard
|
| 39 |
+
|
| 40 |
+
- **Vishal Chauhan** (Computer Science & Engineering, Project Lead)
|
| 41 |
+
- **Prince Mishra** (Computer Science & Engineering, Backend Developer)
|
| 42 |
+
- **Prince Dubey** (Computer Science & Engineering, Security & Testing)
|
| 43 |
+
- **Raksha** (Computer Science & Engineering, Frontend Developer)
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## 🛠️ Technology Stack
|
| 48 |
+
|
| 49 |
+
- **Backend**: FastAPI, Uvicorn, PyTorch, Hugging Face Transformers, OpenCV, NumPy, SciPy
|
| 50 |
+
- **Frontend**: Vanilla HTML5, CSS3 (Modern dark-theme layout with purple gradients & glassmorphism), Vanilla JavaScript
|
| 51 |
+
- **Deployment**: Docker, Hugging Face Spaces
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## 💻 Local Setup and Running
|
| 56 |
+
|
| 57 |
+
To run this application locally on your machine, follow these steps:
|
| 58 |
+
|
| 59 |
+
### Prerequisites
|
| 60 |
+
- Python 3.10+
|
| 61 |
+
- Pip package manager
|
| 62 |
+
|
| 63 |
+
### Installation
|
| 64 |
+
|
| 65 |
+
1. **Clone the repository**:
|
| 66 |
+
```bash
|
| 67 |
+
git clone <repository-url>
|
| 68 |
+
cd imgauth-ai
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
2. **Create and activate a virtual environment**:
|
| 72 |
+
- **Windows (PowerShell)**:
|
| 73 |
+
```powershell
|
| 74 |
+
python -m venv .venv
|
| 75 |
+
.\.venv\Scripts\activate
|
| 76 |
+
```
|
| 77 |
+
- **macOS/Linux**:
|
| 78 |
+
```bash
|
| 79 |
+
python -m venv .venv
|
| 80 |
+
source .venv/bin/activate
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
3. **Install dependencies**:
|
| 84 |
+
```bash
|
| 85 |
+
pip install -r requirements.txt
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
4. **Run the server**:
|
| 89 |
+
```bash
|
| 90 |
+
python run.py
|
| 91 |
+
```
|
| 92 |
+
*The app will start running at:* `http://localhost:5000`
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## 🐳 Running with Docker
|
| 97 |
+
|
| 98 |
+
Alternatively, build and run via Docker:
|
| 99 |
+
|
| 100 |
+
1. **Build the image**:
|
| 101 |
+
```bash
|
| 102 |
+
docker build -t imgauth-ai .
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
2. **Run the container**:
|
| 106 |
+
```bash
|
| 107 |
+
docker run -p 7860:7860 imgauth-ai
|
| 108 |
+
```
|
| 109 |
+
*Open browser to:* `http://localhost:7860`
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## ⚖️ License & Attribution
|
| 114 |
+
|
| 115 |
+
- **Non-Commercial**: This project uses the `Organika/sdxl-detector` model, licensed under CC BY-NC 4.0. It is intended strictly for non-commercial educational and research purposes.
|
| 116 |
+
- **Model Attribution**: All deep learning classifications are handled by model weights published by the Hugging Face community.
|
backend/__init__.py
ADDED
|
File without changes
|
backend/detectors/__init__.py
ADDED
|
File without changes
|
backend/detectors/filename_detector.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
STRONG_AI_KEYWORDS = {
|
| 5 |
+
"midjourney": 70, "dall-e": 70, "dalle": 70, "stable-diffusion": 70,
|
| 6 |
+
"stablediffusion": 70, "sdxl": 70, "sd3": 70, "sd35": 70,
|
| 7 |
+
"firefly": 70, "ideogram": 70, "imagen": 70, "kling": 70,
|
| 8 |
+
"sora": 70, "runway": 70, "pika": 70,
|
| 9 |
+
"chatgpt": 70, "openai": 70, "gpt4": 70, "gpt-4": 70, "gpt4o": 70,
|
| 10 |
+
"gemini": 70, "claude": 70, "perplexity": 70, "copilot": 70,
|
| 11 |
+
"leonardo": 70, "nanobanana": 70, "nano-banana": 70, "flux": 70,
|
| 12 |
+
"pixart": 70, "playground": 70, "kolors": 70, "hunyuan": 70,
|
| 13 |
+
"cogview": 70, "deepfloyd": 70, "kandinsky": 70, "wuerstchen": 70,
|
| 14 |
+
"animagine": 70, "waifu": 60,
|
| 15 |
+
"comfyui": 70, "automatic1111": 70, "a1111": 70, "fooocus": 70,
|
| 16 |
+
"invokeai": 70, "forge": 60,
|
| 17 |
+
"txt2img": 70, "img2img": 70, "t2i": 60, "i2i": 60,
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
MEDIUM_AI_KEYWORDS = {
|
| 21 |
+
"generated": 35, "ai-generated": 35, "aigenerated": 35,
|
| 22 |
+
"fake": 25, "synthetic": 25, "deepfake": 30,
|
| 23 |
+
"aiimage": 30, "ai_image": 30, "ai-image": 30,
|
| 24 |
+
"upscaled": 15, "inpainted": 20, "outpainted": 20,
|
| 25 |
+
"dreamshaper": 25, "juggernaut": 25, "realisticvision": 25,
|
| 26 |
+
"deepdream": 20, "artbreeder": 20, "nightcafe": 20,
|
| 27 |
+
"craiyon": 20, "dreamstudio": 25, "prompthero": 20,
|
| 28 |
+
"civitai": 25, "tensor-art": 20, "tensorart": 20,
|
| 29 |
+
"diffusion": 20, "lora": 15, "checkpoint": 15,
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
REAL_KEYWORDS = {
|
| 33 |
+
"photo": 8, "raw": 8, "dsc": 10, "dscn": 10, "dscf": 10,
|
| 34 |
+
"img_": 8, "img-": 8, "canon": 10, "nikon": 10, "sony": 10,
|
| 35 |
+
"iphone": 10, "samsung": 8, "pixel": 10, "oneplus": 8,
|
| 36 |
+
"screenshot": 5, "scan": 8, "pxl_": 10,
|
| 37 |
+
"snap": 6, "dcim": 10, "camera": 8,
|
| 38 |
+
"fujifilm": 10, "olympus": 10, "panasonic": 10, "leica": 10,
|
| 39 |
+
"gopro": 10, "dji_": 10, "mavic": 10,
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def analyze_filename(filename: str) -> dict:
|
| 44 |
+
name = filename.lower()
|
| 45 |
+
name_no_ext = os.path.splitext(name)[0]
|
| 46 |
+
|
| 47 |
+
strong_hits, medium_hits, real_hits = [], [], []
|
| 48 |
+
strong_ai, medium_ai, real_pts = 0, 0, 0
|
| 49 |
+
signals = []
|
| 50 |
+
|
| 51 |
+
for kw, pts in STRONG_AI_KEYWORDS.items():
|
| 52 |
+
if kw in name_no_ext:
|
| 53 |
+
strong_ai = max(strong_ai, pts)
|
| 54 |
+
strong_hits.append(kw)
|
| 55 |
+
signals.append(f"[AI] Strong AI keyword: '{kw}'")
|
| 56 |
+
|
| 57 |
+
for kw, pts in MEDIUM_AI_KEYWORDS.items():
|
| 58 |
+
if kw in name_no_ext:
|
| 59 |
+
medium_ai = max(medium_ai, pts)
|
| 60 |
+
medium_hits.append(kw)
|
| 61 |
+
signals.append(f"[INFO] Medium AI keyword: '{kw}'")
|
| 62 |
+
|
| 63 |
+
for kw, pts in REAL_KEYWORDS.items():
|
| 64 |
+
if kw in name_no_ext:
|
| 65 |
+
real_pts += pts
|
| 66 |
+
real_hits.append(kw)
|
| 67 |
+
signals.append(f"[REAL] Camera keyword: '{kw}' (+{pts} Real pts)")
|
| 68 |
+
|
| 69 |
+
if re.search(r'[a-f0-9]{16,}', name_no_ext):
|
| 70 |
+
medium_ai = max(medium_ai, 30)
|
| 71 |
+
signals.append("[INFO] Long hex string pattern (common AI export naming)")
|
| 72 |
+
|
| 73 |
+
if re.search(r'[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}', name_no_ext):
|
| 74 |
+
medium_ai = max(medium_ai, 25)
|
| 75 |
+
signals.append("[INFO] UUID v4 pattern detected (common AI platform export)")
|
| 76 |
+
|
| 77 |
+
if re.search(r'(seed|cfg|step|sampler|guidance|scheduler)[_\-]?\d+', name_no_ext):
|
| 78 |
+
strong_ai = max(strong_ai, 70)
|
| 79 |
+
signals.append("[AI] Generation parameter pattern (seed/cfg/step)")
|
| 80 |
+
|
| 81 |
+
if re.search(r'^(0{2,}\d+[\-_]|grid[\-_]|tmp[\-_])', name_no_ext):
|
| 82 |
+
medium_ai = max(medium_ai, 25)
|
| 83 |
+
signals.append("[INFO] WebUI output naming pattern (sequential numbering)")
|
| 84 |
+
|
| 85 |
+
if re.search(r'(1024x1024|512x512|768x768|1024x768|1216x832|832x1216)', name_no_ext):
|
| 86 |
+
medium_ai = max(medium_ai, 20)
|
| 87 |
+
signals.append("[INFO] Common AI resolution in filename")
|
| 88 |
+
|
| 89 |
+
if strong_ai >= 60:
|
| 90 |
+
bucket = "strong_ai_trigger"
|
| 91 |
+
ai_pts = min(strong_ai, 70)
|
| 92 |
+
real_pts = min(real_pts, 10)
|
| 93 |
+
priority_note = "Filename flagged — but pixel analysis will confirm or override."
|
| 94 |
+
elif medium_ai >= 25:
|
| 95 |
+
bucket = "medium_ai_suspicion"
|
| 96 |
+
ai_pts = min(medium_ai, 50)
|
| 97 |
+
real_pts = min(real_pts, 15)
|
| 98 |
+
priority_note = "Suspicious filename — models and forensics will verify."
|
| 99 |
+
else:
|
| 100 |
+
bucket = "neutral_or_real_hint"
|
| 101 |
+
ai_pts = 0
|
| 102 |
+
real_pts = max(real_pts, 5)
|
| 103 |
+
priority_note = "No suspicious filename signals."
|
| 104 |
+
|
| 105 |
+
if signals:
|
| 106 |
+
signals.append("[NOTE] Filename can be easily changed — this layer has low reliability.")
|
| 107 |
+
|
| 108 |
+
return {
|
| 109 |
+
"bucket": bucket,
|
| 110 |
+
"ai_points": ai_pts,
|
| 111 |
+
"real_points": min(real_pts, 25),
|
| 112 |
+
"strong_hits": strong_hits,
|
| 113 |
+
"medium_hits": medium_hits,
|
| 114 |
+
"real_hits": real_hits,
|
| 115 |
+
"signals": signals if signals else ["No suspicious filename patterns detected."],
|
| 116 |
+
"priority_note": priority_note,
|
| 117 |
+
}
|
backend/detectors/image_detector.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import numpy as np
|
| 3 |
+
import cv2
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from scipy.stats import kurtosis as sp_kurtosis
|
| 6 |
+
|
| 7 |
+
MODEL_CACHE = {}
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def load_models():
|
| 11 |
+
from transformers import pipeline
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
device_id = 0 if torch.cuda.is_available() else -1
|
| 15 |
+
print(f"[ImgAuth] Using device: {'GPU (cuda:0)' if device_id == 0 else 'CPU'}")
|
| 16 |
+
|
| 17 |
+
model_ids = [
|
| 18 |
+
("umm_maybe", "umm-maybe/AI-image-detector"),
|
| 19 |
+
("dima806", "dima806/ai_vs_real_image_detection"),
|
| 20 |
+
("organika", "Organika/sdxl-detector"),
|
| 21 |
+
]
|
| 22 |
+
for key, model_id in model_ids:
|
| 23 |
+
if key not in MODEL_CACHE:
|
| 24 |
+
try:
|
| 25 |
+
print(f"[ImgAuth] Loading model: {model_id}")
|
| 26 |
+
MODEL_CACHE[key] = pipeline(
|
| 27 |
+
"image-classification", model=model_id, device=device_id, framework="pt"
|
| 28 |
+
)
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"[ImgAuth] Failed to load {model_id}: {e}")
|
| 31 |
+
MODEL_CACHE[key] = None
|
| 32 |
+
return MODEL_CACHE
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def run_model(pipe, img):
|
| 36 |
+
try:
|
| 37 |
+
preds = pipe(img)
|
| 38 |
+
ai_s, real_s = 0.0, 0.0
|
| 39 |
+
for p in preds:
|
| 40 |
+
label = p["label"].lower()
|
| 41 |
+
if any(k in label for k in ["ai", "fake", "artificial", "generated", "synthetic"]):
|
| 42 |
+
ai_s = max(ai_s, p["score"])
|
| 43 |
+
elif any(k in label for k in ["real", "human", "natural", "authentic"]):
|
| 44 |
+
real_s = max(real_s, p["score"])
|
| 45 |
+
if ai_s == 0 and real_s == 0 and len(preds) >= 2:
|
| 46 |
+
ai_s = preds[0]["score"]
|
| 47 |
+
real_s = preds[1]["score"]
|
| 48 |
+
elif ai_s == 0 and real_s == 0:
|
| 49 |
+
ai_s, real_s = 0.5, 0.5
|
| 50 |
+
total = ai_s + real_s or 1
|
| 51 |
+
return {
|
| 52 |
+
"ai_prob": round(ai_s / total, 4),
|
| 53 |
+
"real_prob": round(real_s / total, 4),
|
| 54 |
+
"raw": preds,
|
| 55 |
+
}
|
| 56 |
+
except Exception as e:
|
| 57 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "raw": [], "error": str(e)}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def run_model_multiscale(pipe, img_full):
|
| 61 |
+
img_512 = img_full.copy()
|
| 62 |
+
img_512.thumbnail((512, 512))
|
| 63 |
+
result_512 = run_model(pipe, img_512)
|
| 64 |
+
|
| 65 |
+
w, h = img_full.size
|
| 66 |
+
import torch
|
| 67 |
+
if w > 600 and h > 600 and torch.cuda.is_available():
|
| 68 |
+
crop_size = min(w, h, 384)
|
| 69 |
+
cx, cy = w // 2, h // 2
|
| 70 |
+
half = crop_size // 2
|
| 71 |
+
center_crop = img_full.crop((cx - half, cy - half, cx + half, cy + half))
|
| 72 |
+
result_crop = run_model(pipe, center_crop)
|
| 73 |
+
ai_prob = result_512["ai_prob"] * 0.65 + result_crop["ai_prob"] * 0.35
|
| 74 |
+
real_prob = result_512["real_prob"] * 0.65 + result_crop["real_prob"] * 0.35
|
| 75 |
+
return {
|
| 76 |
+
"ai_prob": round(ai_prob, 4),
|
| 77 |
+
"real_prob": round(real_prob, 4),
|
| 78 |
+
"raw": result_512["raw"],
|
| 79 |
+
}
|
| 80 |
+
return result_512
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def noise_kurtosis_analysis(img):
|
| 84 |
+
try:
|
| 85 |
+
gray = np.array(img.convert("L"), dtype=np.float64)
|
| 86 |
+
residual = cv2.Laplacian(gray, cv2.CV_64F)
|
| 87 |
+
flat = residual.flatten()
|
| 88 |
+
flat = flat[np.abs(flat) > 0.5]
|
| 89 |
+
if len(flat) < 100:
|
| 90 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "kurtosis": 0.0,
|
| 91 |
+
"detail": "Insufficient noise data"}
|
| 92 |
+
|
| 93 |
+
k = float(sp_kurtosis(flat, fisher=True))
|
| 94 |
+
|
| 95 |
+
if k > 5.0:
|
| 96 |
+
ai_prob = 0.12
|
| 97 |
+
elif k > 2.5:
|
| 98 |
+
ai_prob = 0.28
|
| 99 |
+
elif k > 1.0:
|
| 100 |
+
ai_prob = 0.42
|
| 101 |
+
elif k > 0.0:
|
| 102 |
+
ai_prob = 0.55
|
| 103 |
+
elif k > -0.5:
|
| 104 |
+
ai_prob = 0.65
|
| 105 |
+
else:
|
| 106 |
+
ai_prob = 0.78
|
| 107 |
+
|
| 108 |
+
label = "leptokurtic (real-like)" if k > 1.5 else "platykurtic (AI-like)" if k < 0 else "borderline"
|
| 109 |
+
return {
|
| 110 |
+
"ai_prob": round(ai_prob, 4),
|
| 111 |
+
"real_prob": round(1 - ai_prob, 4),
|
| 112 |
+
"kurtosis": round(k, 3),
|
| 113 |
+
"detail": f"Excess kurtosis={k:.3f} -> {label}",
|
| 114 |
+
}
|
| 115 |
+
except Exception as e:
|
| 116 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "kurtosis": 0.0,
|
| 117 |
+
"detail": f"Error: {str(e)[:60]}"}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
import torch
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def extract_vit_features_and_attentions(img):
|
| 124 |
+
try:
|
| 125 |
+
pipe = MODEL_CACHE.get("umm_maybe")
|
| 126 |
+
if not pipe or not hasattr(pipe, "model") or not hasattr(pipe, "image_processor"):
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
model = pipe.model
|
| 130 |
+
processor = pipe.image_processor
|
| 131 |
+
|
| 132 |
+
inputs = processor(images=img, return_tensors="pt")
|
| 133 |
+
# Match device of inputs to device of model (CPU or GPU)
|
| 134 |
+
model_device = next(model.parameters()).device
|
| 135 |
+
inputs = {k: v.to(model_device) for k, v in inputs.items()}
|
| 136 |
+
|
| 137 |
+
with torch.no_grad():
|
| 138 |
+
outputs = model(**inputs, output_attentions=True, output_hidden_states=True)
|
| 139 |
+
|
| 140 |
+
return {
|
| 141 |
+
"logits": outputs.logits,
|
| 142 |
+
"attentions": outputs.attentions,
|
| 143 |
+
"hidden_states": outputs.hidden_states
|
| 144 |
+
}
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"[ImgAuth] Feature/Attention extraction error: {e}")
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def deep_feature_inconsistency_analysis(vit_data):
|
| 151 |
+
try:
|
| 152 |
+
if not vit_data or "hidden_states" not in vit_data:
|
| 153 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "variance": 0.0, "detail": "ViT data unavailable", "cos_dist": None, "grid_w": 0, "grid_h": 0, "patch_features": None}
|
| 154 |
+
|
| 155 |
+
last_hidden = vit_data["hidden_states"][-1][0].cpu()
|
| 156 |
+
N = last_hidden.shape[0]
|
| 157 |
+
|
| 158 |
+
import math
|
| 159 |
+
root_N = int(round(math.sqrt(N)))
|
| 160 |
+
if root_N * root_N == N:
|
| 161 |
+
patch_features = last_hidden
|
| 162 |
+
grid_w = grid_h = root_N
|
| 163 |
+
else:
|
| 164 |
+
root_N_minus_1 = int(round(math.sqrt(N - 1)))
|
| 165 |
+
if root_N_minus_1 * root_N_minus_1 == N - 1:
|
| 166 |
+
patch_features = last_hidden[1:, :]
|
| 167 |
+
grid_w = grid_h = root_N_minus_1
|
| 168 |
+
else:
|
| 169 |
+
patch_features = last_hidden[1:, :]
|
| 170 |
+
N_patches = N - 1
|
| 171 |
+
grid_w = int(math.sqrt(N_patches))
|
| 172 |
+
grid_h = N_patches // grid_w
|
| 173 |
+
patch_features = patch_features[:grid_w * grid_h, :]
|
| 174 |
+
|
| 175 |
+
mean_feat = torch.mean(patch_features, dim=0, keepdim=True)
|
| 176 |
+
cos_sim = torch.nn.functional.cosine_similarity(patch_features, mean_feat, dim=1)
|
| 177 |
+
cos_dist = 1.0 - cos_sim
|
| 178 |
+
|
| 179 |
+
dist_variance = float(torch.var(cos_dist).item())
|
| 180 |
+
|
| 181 |
+
if dist_variance > 0.0035:
|
| 182 |
+
ai_prob = 0.76
|
| 183 |
+
detail = f"High deep feature inconsistency (variance={dist_variance:.6f})"
|
| 184 |
+
elif dist_variance > 0.0018:
|
| 185 |
+
ai_prob = 0.62
|
| 186 |
+
detail = f"Moderate deep feature inconsistency (variance={dist_variance:.6f})"
|
| 187 |
+
elif dist_variance > 0.0006:
|
| 188 |
+
ai_prob = 0.44
|
| 189 |
+
detail = f"Normal feature consistency (variance={dist_variance:.6f})"
|
| 190 |
+
else:
|
| 191 |
+
ai_prob = 0.28
|
| 192 |
+
detail = f"High feature uniformity (variance={dist_variance:.6f})"
|
| 193 |
+
|
| 194 |
+
return {
|
| 195 |
+
"ai_prob": round(ai_prob, 4),
|
| 196 |
+
"real_prob": round(1.0 - ai_prob, 4),
|
| 197 |
+
"variance": round(dist_variance, 6),
|
| 198 |
+
"cos_dist": cos_dist,
|
| 199 |
+
"grid_w": grid_w,
|
| 200 |
+
"grid_h": grid_h,
|
| 201 |
+
"patch_features": patch_features,
|
| 202 |
+
"detail": detail
|
| 203 |
+
}
|
| 204 |
+
except Exception as e:
|
| 205 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "variance": 0.0, "detail": f"Error: {str(e)[:60]}", "cos_dist": None, "grid_w": 0, "grid_h": 0, "patch_features": None}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _encode_overlay_to_base64(img_bgr):
|
| 209 |
+
"""Encode a BGR numpy array to a Base64 JPEG data URL string."""
|
| 210 |
+
success, buffer = cv2.imencode(".jpg", img_bgr, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
| 211 |
+
if not success:
|
| 212 |
+
return None
|
| 213 |
+
import base64
|
| 214 |
+
b64 = base64.b64encode(buffer).decode("utf-8")
|
| 215 |
+
return f"data:image/jpeg;base64,{b64}"
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def generate_heatmap_overlay(img, vit_data, cos_dist_tensor, grid_w, grid_h, patch_features):
|
| 219 |
+
"""Generate attention and DFI heatmap overlays entirely in memory.
|
| 220 |
+
Returns Base64-encoded JPEG data URL strings (no files written to disk).
|
| 221 |
+
"""
|
| 222 |
+
try:
|
| 223 |
+
w, h = img.size
|
| 224 |
+
img_np = np.array(img.convert("RGB"))
|
| 225 |
+
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
| 226 |
+
|
| 227 |
+
att_b64 = None
|
| 228 |
+
dfi_b64 = None
|
| 229 |
+
|
| 230 |
+
# ── Attention Heatmap ────────────────────────────────────────────────
|
| 231 |
+
attentions = vit_data.get("attentions") if vit_data else None
|
| 232 |
+
att_grid = None
|
| 233 |
+
if attentions:
|
| 234 |
+
try:
|
| 235 |
+
last_layer_att = attentions[-1][0].cpu()
|
| 236 |
+
if last_layer_att.ndim == 3:
|
| 237 |
+
avg_att = torch.mean(last_layer_att, dim=0)
|
| 238 |
+
if avg_att.shape[0] == patch_features.shape[0]:
|
| 239 |
+
cls_attention = avg_att.mean(dim=0)
|
| 240 |
+
else:
|
| 241 |
+
cls_attention = avg_att[0, 1:]
|
| 242 |
+
if cls_attention.numel() == grid_w * grid_h:
|
| 243 |
+
att_grid = cls_attention.reshape(grid_w, grid_h).numpy()
|
| 244 |
+
except Exception as e:
|
| 245 |
+
print(f"[ImgAuth] Attention extraction error: {e}")
|
| 246 |
+
|
| 247 |
+
# Fallback to feature norm if attention parsing fails
|
| 248 |
+
if att_grid is None and patch_features is not None and grid_w > 0 and grid_h > 0:
|
| 249 |
+
try:
|
| 250 |
+
norm_feat = torch.norm(patch_features, dim=1)
|
| 251 |
+
att_grid = norm_feat.reshape(grid_w, grid_h).numpy()
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f"[ImgAuth] Feature norm fallback error: {e}")
|
| 254 |
+
|
| 255 |
+
if att_grid is not None:
|
| 256 |
+
g_min, g_max = att_grid.min(), att_grid.max()
|
| 257 |
+
att_grid = (att_grid - g_min) / (g_max - g_min + 1e-8)
|
| 258 |
+
att_resized = cv2.resize((att_grid * 255).astype(np.uint8), (w, h), interpolation=cv2.INTER_CUBIC)
|
| 259 |
+
heatmap_att = cv2.applyColorMap(att_resized, cv2.COLORMAP_JET)
|
| 260 |
+
overlay_att = cv2.addWeighted(img_bgr, 0.6, heatmap_att, 0.4, 0)
|
| 261 |
+
att_b64 = _encode_overlay_to_base64(overlay_att)
|
| 262 |
+
|
| 263 |
+
# ── DFI Heatmap ���─────────────────────────────────────────────────────
|
| 264 |
+
if cos_dist_tensor is not None and grid_w > 0 and grid_h > 0:
|
| 265 |
+
dist_grid = cos_dist_tensor.reshape(grid_w, grid_h).numpy()
|
| 266 |
+
g_min, g_max = dist_grid.min(), dist_grid.max()
|
| 267 |
+
dist_grid = (dist_grid - g_min) / (g_max - g_min + 1e-8)
|
| 268 |
+
dist_resized = cv2.resize((dist_grid * 255).astype(np.uint8), (w, h), interpolation=cv2.INTER_CUBIC)
|
| 269 |
+
heatmap_dfi = cv2.applyColorMap(dist_resized, cv2.COLORMAP_JET)
|
| 270 |
+
overlay_dfi = cv2.addWeighted(img_bgr, 0.6, heatmap_dfi, 0.4, 0)
|
| 271 |
+
dfi_b64 = _encode_overlay_to_base64(overlay_dfi)
|
| 272 |
+
|
| 273 |
+
return att_b64, dfi_b64
|
| 274 |
+
except Exception as e:
|
| 275 |
+
print(f"[ImgAuth] Error generating heatmaps: {e}")
|
| 276 |
+
return None, None
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def fft_spectral_analysis(img):
|
| 280 |
+
try:
|
| 281 |
+
gray = np.array(img.convert("L"), dtype=np.float64)
|
| 282 |
+
size = min(gray.shape[0], gray.shape[1], 512)
|
| 283 |
+
gray = cv2.resize(gray, (size, size))
|
| 284 |
+
|
| 285 |
+
f_transform = np.fft.fft2(gray)
|
| 286 |
+
f_shift = np.fft.fftshift(f_transform)
|
| 287 |
+
magnitude = np.log1p(np.abs(f_shift))
|
| 288 |
+
|
| 289 |
+
center = size // 2
|
| 290 |
+
mask = np.ones_like(magnitude, dtype=bool)
|
| 291 |
+
mask[center - 5:center + 5, center - 5:center + 5] = False
|
| 292 |
+
outer_mag = magnitude[mask]
|
| 293 |
+
|
| 294 |
+
mean_mag = np.mean(outer_mag)
|
| 295 |
+
std_mag = np.std(outer_mag)
|
| 296 |
+
spike_threshold = mean_mag + 5.0 * std_mag
|
| 297 |
+
spike_count = np.sum(magnitude[mask] > spike_threshold)
|
| 298 |
+
spike_ratio = spike_count / len(outer_mag) if len(outer_mag) > 0 else 0
|
| 299 |
+
sr = float(spike_ratio)
|
| 300 |
+
|
| 301 |
+
if sr > 0.01:
|
| 302 |
+
ai_prob = 0.72
|
| 303 |
+
detail = f"Periodic artifacts detected (spike ratio={sr:.4f})"
|
| 304 |
+
elif sr > 0.005:
|
| 305 |
+
ai_prob = 0.58
|
| 306 |
+
detail = f"Minor spectral anomalies ({sr:.4f})"
|
| 307 |
+
elif sr > 0.002:
|
| 308 |
+
ai_prob = 0.48
|
| 309 |
+
detail = f"Faint spectral patterns ({sr:.4f})"
|
| 310 |
+
else:
|
| 311 |
+
ai_prob = 0.38
|
| 312 |
+
detail = f"Clean spectrum (no periodic artifacts)"
|
| 313 |
+
|
| 314 |
+
return {
|
| 315 |
+
"ai_prob": round(ai_prob, 4),
|
| 316 |
+
"real_prob": round(1 - ai_prob, 4),
|
| 317 |
+
"spike_ratio": round(sr, 5),
|
| 318 |
+
"detail": detail,
|
| 319 |
+
}
|
| 320 |
+
except Exception as e:
|
| 321 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "spike_ratio": 0.0,
|
| 322 |
+
"detail": f"Error: {str(e)[:60]}"}
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def color_histogram_analysis(img):
|
| 326 |
+
try:
|
| 327 |
+
rgb = np.array(img.convert("RGB"))
|
| 328 |
+
roughness_scores = []
|
| 329 |
+
for channel in range(3):
|
| 330 |
+
hist, _ = np.histogram(rgb[:, :, channel], bins=64, range=(0, 256))
|
| 331 |
+
hist = hist.astype(np.float64)
|
| 332 |
+
hist /= (hist.sum() + 1e-10)
|
| 333 |
+
diffs = np.diff(hist)
|
| 334 |
+
roughness_scores.append(float(np.std(diffs)))
|
| 335 |
+
|
| 336 |
+
avg_roughness = np.mean(roughness_scores)
|
| 337 |
+
|
| 338 |
+
if avg_roughness > 0.010:
|
| 339 |
+
ai_prob = 0.25
|
| 340 |
+
detail = f"Natural histogram roughness ({avg_roughness:.5f})"
|
| 341 |
+
elif avg_roughness > 0.006:
|
| 342 |
+
ai_prob = 0.38
|
| 343 |
+
detail = f"Moderate histogram roughness ({avg_roughness:.5f})"
|
| 344 |
+
elif avg_roughness > 0.003:
|
| 345 |
+
ai_prob = 0.52
|
| 346 |
+
detail = f"Smooth histogram ({avg_roughness:.5f}) — possibly synthetic"
|
| 347 |
+
else:
|
| 348 |
+
ai_prob = 0.68
|
| 349 |
+
detail = f"Very smooth histogram ({avg_roughness:.5f}) — likely AI"
|
| 350 |
+
|
| 351 |
+
return {
|
| 352 |
+
"ai_prob": round(ai_prob, 4),
|
| 353 |
+
"real_prob": round(1 - ai_prob, 4),
|
| 354 |
+
"roughness": round(avg_roughness, 6),
|
| 355 |
+
"detail": detail,
|
| 356 |
+
}
|
| 357 |
+
except Exception as e:
|
| 358 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "roughness": 0.0,
|
| 359 |
+
"detail": f"Error: {str(e)[:60]}"}
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def jpeg_ghost_analysis(img):
|
| 363 |
+
try:
|
| 364 |
+
if img.format not in ("JPEG", "JPG", None):
|
| 365 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "detail": "Not a JPEG"}
|
| 366 |
+
|
| 367 |
+
rgb = np.array(img.convert("RGB"), dtype=np.float64)
|
| 368 |
+
ghost_scores = []
|
| 369 |
+
for q in [60, 70, 80]:
|
| 370 |
+
buf = io.BytesIO()
|
| 371 |
+
img.save(buf, "JPEG", quality=q)
|
| 372 |
+
buf.seek(0)
|
| 373 |
+
recomp = np.array(Image.open(buf).convert("RGB"), dtype=np.float64)
|
| 374 |
+
diff = np.abs(rgb - recomp)
|
| 375 |
+
ghost_scores.append(float(np.mean(diff)))
|
| 376 |
+
|
| 377 |
+
min_ghost = min(ghost_scores)
|
| 378 |
+
max_ghost = max(ghost_scores)
|
| 379 |
+
spread = max_ghost - min_ghost
|
| 380 |
+
|
| 381 |
+
if spread < 1.0:
|
| 382 |
+
ai_prob = 0.58
|
| 383 |
+
detail = f"Low ghost spread ({spread:.2f}) — uniform compression (possibly synthetic)"
|
| 384 |
+
elif spread < 3.0:
|
| 385 |
+
ai_prob = 0.45
|
| 386 |
+
detail = f"Normal ghost spread ({spread:.2f})"
|
| 387 |
+
else:
|
| 388 |
+
ai_prob = 0.38
|
| 389 |
+
detail = f"High ghost spread ({spread:.2f}) — natural re-compression history"
|
| 390 |
+
|
| 391 |
+
return {
|
| 392 |
+
"ai_prob": round(ai_prob, 4),
|
| 393 |
+
"real_prob": round(1 - ai_prob, 4),
|
| 394 |
+
"ghost_spread": round(spread, 3),
|
| 395 |
+
"detail": detail,
|
| 396 |
+
}
|
| 397 |
+
except Exception as e:
|
| 398 |
+
return {"ai_prob": 0.5, "real_prob": 0.5, "ghost_spread": 0.0,
|
| 399 |
+
"detail": f"Error: {str(e)[:60]}"}
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def analyze_image_models(image_bytes: bytes) -> dict:
|
| 403 |
+
img_full = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
| 404 |
+
|
| 405 |
+
models = load_models()
|
| 406 |
+
votes = []
|
| 407 |
+
signals = []
|
| 408 |
+
|
| 409 |
+
# 1. Run ViT Feature Extraction and Attentions
|
| 410 |
+
vit_data = extract_vit_features_and_attentions(img_full)
|
| 411 |
+
|
| 412 |
+
model_info = [
|
| 413 |
+
("umm_maybe", "umm-maybe ViT", 0.25),
|
| 414 |
+
("dima806", "dima806 CNN", 0.25),
|
| 415 |
+
("organika", "Organika SDXL-Detector", 0.25),
|
| 416 |
+
]
|
| 417 |
+
active_dl_weight = 0.0
|
| 418 |
+
for key, name, w in model_info:
|
| 419 |
+
if models.get(key):
|
| 420 |
+
r = run_model_multiscale(models[key], img_full)
|
| 421 |
+
votes.append({
|
| 422 |
+
"detector": name, "type": "deep_learning",
|
| 423 |
+
"ai_prob": r["ai_prob"], "real_prob": r["real_prob"], "weight": w,
|
| 424 |
+
})
|
| 425 |
+
active_dl_weight += w
|
| 426 |
+
signals.append(f"{name}: {int(r['ai_prob'] * 100)}% AI probability")
|
| 427 |
+
else:
|
| 428 |
+
signals.append(f"{name}: unavailable")
|
| 429 |
+
|
| 430 |
+
if active_dl_weight == 0:
|
| 431 |
+
pass
|
| 432 |
+
else:
|
| 433 |
+
for v in votes:
|
| 434 |
+
if v["type"] == "deep_learning":
|
| 435 |
+
v["weight"] = v["weight"] * (0.75 / active_dl_weight)
|
| 436 |
+
|
| 437 |
+
# 2. Run Noise Kurtosis
|
| 438 |
+
nk = noise_kurtosis_analysis(img_full)
|
| 439 |
+
votes.append({
|
| 440 |
+
"detector": "Noise Kurtosis Analysis", "type": "forensic",
|
| 441 |
+
"ai_prob": nk["ai_prob"], "real_prob": nk["real_prob"], "weight": 0.08,
|
| 442 |
+
"detail": nk.get("detail", ""), "kurtosis": nk.get("kurtosis", 0),
|
| 443 |
+
})
|
| 444 |
+
signals.append(f"Noise Kurtosis: {nk['detail']}")
|
| 445 |
+
|
| 446 |
+
# 3. Run Deep Feature Inconsistency (DFI) instead of ELA
|
| 447 |
+
dfi = deep_feature_inconsistency_analysis(vit_data)
|
| 448 |
+
votes.append({
|
| 449 |
+
"detector": "Deep Feature Inconsistency (DFI)", "type": "forensic",
|
| 450 |
+
"ai_prob": dfi["ai_prob"], "real_prob": dfi["real_prob"], "weight": 0.07,
|
| 451 |
+
"detail": dfi.get("detail", ""), "variance": dfi.get("variance", 0),
|
| 452 |
+
})
|
| 453 |
+
signals.append(f"DFI: {dfi['detail']}")
|
| 454 |
+
|
| 455 |
+
# 4. Run FFT Spectral
|
| 456 |
+
fft = fft_spectral_analysis(img_full)
|
| 457 |
+
votes.append({
|
| 458 |
+
"detector": "FFT Spectral Analysis", "type": "forensic",
|
| 459 |
+
"ai_prob": fft["ai_prob"], "real_prob": fft["real_prob"], "weight": 0.05,
|
| 460 |
+
"detail": fft.get("detail", ""), "spike_ratio": fft.get("spike_ratio", 0),
|
| 461 |
+
})
|
| 462 |
+
signals.append(f"FFT: {fft['detail']}")
|
| 463 |
+
|
| 464 |
+
# 5. Run Color Histogram
|
| 465 |
+
ch = color_histogram_analysis(img_full)
|
| 466 |
+
votes.append({
|
| 467 |
+
"detector": "Color Histogram Analysis", "type": "forensic",
|
| 468 |
+
"ai_prob": ch["ai_prob"], "real_prob": ch["real_prob"], "weight": 0.03,
|
| 469 |
+
"detail": ch.get("detail", ""), "roughness": ch.get("roughness", 0),
|
| 470 |
+
})
|
| 471 |
+
signals.append(f"Color Histogram: {ch['detail']}")
|
| 472 |
+
|
| 473 |
+
# 6. Run JPEG Ghost
|
| 474 |
+
jg = jpeg_ghost_analysis(img_full)
|
| 475 |
+
votes.append({
|
| 476 |
+
"detector": "JPEG Ghost Analysis", "type": "forensic",
|
| 477 |
+
"ai_prob": jg["ai_prob"], "real_prob": jg["real_prob"], "weight": 0.02,
|
| 478 |
+
"detail": jg.get("detail", ""), "ghost_spread": jg.get("ghost_spread", 0),
|
| 479 |
+
})
|
| 480 |
+
signals.append(f"JPEG Ghost: {jg['detail']}")
|
| 481 |
+
|
| 482 |
+
# Generate explainability overlays (in-memory Base64, no disk writes)
|
| 483 |
+
att_path, dfi_path = generate_heatmap_overlay(
|
| 484 |
+
img_full,
|
| 485 |
+
vit_data,
|
| 486 |
+
dfi.get("cos_dist"),
|
| 487 |
+
dfi.get("grid_w", 0),
|
| 488 |
+
dfi.get("grid_h", 0),
|
| 489 |
+
dfi.get("patch_features"),
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
# Delete non-serializable PyTorch tensors from returned dictionary
|
| 493 |
+
for k in ["cos_dist", "patch_features"]:
|
| 494 |
+
if k in dfi:
|
| 495 |
+
del dfi[k]
|
| 496 |
+
|
| 497 |
+
total_w = sum(v["weight"] for v in votes) or 1
|
| 498 |
+
w_ai = sum(v["ai_prob"] * v["weight"] for v in votes) / total_w
|
| 499 |
+
w_real = sum(v["real_prob"] * v["weight"] for v in votes) / total_w
|
| 500 |
+
tot = w_ai + w_real or 1
|
| 501 |
+
w_ai /= tot
|
| 502 |
+
w_real /= tot
|
| 503 |
+
|
| 504 |
+
dl_pct = int(min(active_dl_weight / total_w * 100, 100))
|
| 505 |
+
forensic_pct = 100 - dl_pct
|
| 506 |
+
|
| 507 |
+
return {
|
| 508 |
+
"ai_points": int(w_ai * 100),
|
| 509 |
+
"real_points": int(w_real * 100),
|
| 510 |
+
"weighted_ai_prob": round(w_ai, 4),
|
| 511 |
+
"votes": votes,
|
| 512 |
+
"signals": signals,
|
| 513 |
+
"forensics": {
|
| 514 |
+
"kurtosis": nk, "dfi": dfi, "fft": fft,
|
| 515 |
+
"color_histogram": ch, "jpeg_ghost": jg,
|
| 516 |
+
},
|
| 517 |
+
"attention_heatmap": att_path,
|
| 518 |
+
"dfi_heatmap": dfi_path,
|
| 519 |
+
"priority_note": f"DL-dominant ensemble: 3 models ({dl_pct}%) + 5 forensics ({forensic_pct}%).",
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
|
backend/detectors/metadata_detector.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def analyze_metadata(image_bytes: bytes) -> dict:
|
| 5 |
+
ai_pts, real_pts = 0, 0
|
| 6 |
+
signals = []
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
from PIL import Image
|
| 10 |
+
from PIL.ExifTags import TAGS
|
| 11 |
+
|
| 12 |
+
img = Image.open(io.BytesIO(image_bytes))
|
| 13 |
+
|
| 14 |
+
exif_data = {}
|
| 15 |
+
raw = None
|
| 16 |
+
try:
|
| 17 |
+
raw = img._getexif()
|
| 18 |
+
if raw:
|
| 19 |
+
exif_data = {
|
| 20 |
+
TAGS.get(k, k): v
|
| 21 |
+
for k, v in raw.items()
|
| 22 |
+
if isinstance(v, (str, int, float, bytes))
|
| 23 |
+
}
|
| 24 |
+
except Exception:
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
camera_fields_found = 0
|
| 28 |
+
for field in ["Make", "Model", "LensMake", "LensModel",
|
| 29 |
+
"FocalLength", "ExposureTime", "FNumber", "ISOSpeedRatings"]:
|
| 30 |
+
if field in exif_data:
|
| 31 |
+
camera_fields_found += 1
|
| 32 |
+
|
| 33 |
+
if camera_fields_found >= 3:
|
| 34 |
+
real_pts += 25
|
| 35 |
+
signals.append(f"[REAL] {camera_fields_found} camera EXIF fields found (+25 Real)")
|
| 36 |
+
elif camera_fields_found >= 1:
|
| 37 |
+
real_pts += 10
|
| 38 |
+
signals.append(f"[REAL] {camera_fields_found} camera EXIF field(s) found (+10 Real)")
|
| 39 |
+
|
| 40 |
+
gps_tags = [k for k in (exif_data or {}) if isinstance(k, str) and "GPS" in k]
|
| 41 |
+
if not gps_tags and raw:
|
| 42 |
+
if 0x8825 in raw or 34853 in raw:
|
| 43 |
+
gps_tags = ["GPSInfo"]
|
| 44 |
+
if gps_tags:
|
| 45 |
+
real_pts += 15
|
| 46 |
+
signals.append("[REAL] GPS geolocation data present (+15 Real)")
|
| 47 |
+
|
| 48 |
+
software = str(exif_data.get("Software", "")).lower()
|
| 49 |
+
ai_software_keywords = [
|
| 50 |
+
"stable diffusion", "midjourney", "comfyui", "diffusers",
|
| 51 |
+
"drawthings", "invoke", "fooocus", "flux", "dall-e", "dalle",
|
| 52 |
+
"nanobanana", "nano banana", "pixart", "playground",
|
| 53 |
+
"automatic1111", "a1111", "novelai", "tensorrt",
|
| 54 |
+
"chatgpt", "gemini", "copilot",
|
| 55 |
+
]
|
| 56 |
+
for kw in ai_software_keywords:
|
| 57 |
+
if kw in software:
|
| 58 |
+
ai_pts += 35
|
| 59 |
+
signals.append(f"[AI] EXIF Software = AI tool '{kw}' (+35 AI)")
|
| 60 |
+
break
|
| 61 |
+
|
| 62 |
+
photo_editors = ["photoshop", "lightroom", "gimp", "capture one",
|
| 63 |
+
"darktable", "rawtherapee", "snapseed"]
|
| 64 |
+
for ed in photo_editors:
|
| 65 |
+
if ed in software:
|
| 66 |
+
real_pts += 5
|
| 67 |
+
signals.append(f"[REAL] Photo editor '{ed}' in Software tag (+5 Real)")
|
| 68 |
+
break
|
| 69 |
+
|
| 70 |
+
if img.format == "PNG":
|
| 71 |
+
png_keys = [str(k).lower() for k in img.info.keys()]
|
| 72 |
+
ai_png_keys = ["prompt", "parameters", "workflow", "invokeai",
|
| 73 |
+
"comfy", "negative_prompt", "steps", "sampler",
|
| 74 |
+
"cfg_scale", "seed", "model", "generation_data"]
|
| 75 |
+
for k in ai_png_keys:
|
| 76 |
+
if any(k in pk for pk in png_keys):
|
| 77 |
+
ai_pts += 35
|
| 78 |
+
signals.append(f"[AI] PNG metadata key '{k}' found (AI generator output) (+35 AI)")
|
| 79 |
+
break
|
| 80 |
+
|
| 81 |
+
for key, value in img.info.items():
|
| 82 |
+
if isinstance(value, str) and len(value) > 50:
|
| 83 |
+
val_lower = value.lower()
|
| 84 |
+
if any(w in val_lower for w in ["negative prompt", "cfg scale",
|
| 85 |
+
"seed:", "steps:", "sampler"]):
|
| 86 |
+
ai_pts += 30
|
| 87 |
+
signals.append("[AI] PNG text contains generation parameters (+30 AI)")
|
| 88 |
+
break
|
| 89 |
+
|
| 90 |
+
if img.format in ("JPEG", "JPG") and not exif_data:
|
| 91 |
+
ai_pts += 5
|
| 92 |
+
signals.append("[INFO] JPEG has no EXIF — weak signal, common in shared photos (+5 AI)")
|
| 93 |
+
|
| 94 |
+
if img.format in ("JPEG", "JPG") and not exif_data and not raw:
|
| 95 |
+
try:
|
| 96 |
+
data = image_bytes
|
| 97 |
+
if data[0:2] == b'\xff\xd8':
|
| 98 |
+
has_app1 = b'\xff\xe1' in data[:20]
|
| 99 |
+
if not has_app1:
|
| 100 |
+
ai_pts += 3
|
| 101 |
+
signals.append("[INFO] JPEG missing APP1 marker — metadata may have been stripped (+3 AI)")
|
| 102 |
+
except Exception:
|
| 103 |
+
pass
|
| 104 |
+
|
| 105 |
+
xmp = img.info.get("xmp", b"")
|
| 106 |
+
if isinstance(xmp, bytes):
|
| 107 |
+
xmp = xmp.decode("utf-8", errors="ignore")
|
| 108 |
+
|
| 109 |
+
if xmp:
|
| 110 |
+
if "c2pa" in xmp.lower() or "contentcredentials" in xmp.lower():
|
| 111 |
+
real_pts += 20
|
| 112 |
+
signals.append("[REAL] C2PA content credentials found (+20 Real)")
|
| 113 |
+
|
| 114 |
+
if "ai-generated" in xmp.lower() or "ai_generated" in xmp.lower():
|
| 115 |
+
ai_pts += 25
|
| 116 |
+
signals.append("[AI] XMP marks image as AI-generated (+25 AI)")
|
| 117 |
+
|
| 118 |
+
if "photoshop" in xmp.lower() or "lightroom" in xmp.lower():
|
| 119 |
+
real_pts += 5
|
| 120 |
+
signals.append("[REAL] Adobe editing history in XMP (+5 Real)")
|
| 121 |
+
|
| 122 |
+
w, h = img.size
|
| 123 |
+
common_ai_sizes = [
|
| 124 |
+
(512, 512), (768, 768), (1024, 1024), (2048, 2048),
|
| 125 |
+
(1024, 768), (768, 1024),
|
| 126 |
+
(1216, 832), (832, 1216),
|
| 127 |
+
(1344, 768), (768, 1344),
|
| 128 |
+
(1152, 896), (896, 1152),
|
| 129 |
+
(1536, 1024), (1024, 1536),
|
| 130 |
+
(1024, 576), (576, 1024),
|
| 131 |
+
]
|
| 132 |
+
if (w, h) in common_ai_sizes:
|
| 133 |
+
ai_pts += 8
|
| 134 |
+
signals.append(f"[INFO] Resolution {w}x{h} is common AI generation size (+8 AI)")
|
| 135 |
+
|
| 136 |
+
if w == h and w >= 512:
|
| 137 |
+
ai_pts += 5
|
| 138 |
+
signals.append(f"[INFO] Perfect square ({w}x{h}) — uncommon for cameras (+5 AI)")
|
| 139 |
+
|
| 140 |
+
if img.format in ("JPEG", "JPG"):
|
| 141 |
+
try:
|
| 142 |
+
qtables = img.quantization
|
| 143 |
+
if qtables:
|
| 144 |
+
avg_quant = sum(sum(t) for t in qtables.values()) / sum(len(t) for t in qtables.values())
|
| 145 |
+
if avg_quant < 3:
|
| 146 |
+
ai_pts += 5
|
| 147 |
+
signals.append("[INFO] Extremely high JPEG quality (typical of AI output) (+5 AI)")
|
| 148 |
+
except Exception:
|
| 149 |
+
pass
|
| 150 |
+
|
| 151 |
+
if img.mode == "RGBA":
|
| 152 |
+
if img.format in ("JPEG", "JPG"):
|
| 153 |
+
signals.append("[INFO] Image has alpha channel in JPEG (unusual)")
|
| 154 |
+
|
| 155 |
+
except Exception as e:
|
| 156 |
+
signals.append(f"[ERROR] Metadata error: {str(e)[:60]}")
|
| 157 |
+
|
| 158 |
+
return {
|
| 159 |
+
"ai_points": min(ai_pts, 65),
|
| 160 |
+
"real_points": min(real_pts, 45),
|
| 161 |
+
"signals": signals or ["No strong metadata signals found."],
|
| 162 |
+
"weight": 0.25,
|
| 163 |
+
}
|
backend/detectors/scoring_engine.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def calculate_final_score(fn, md, ml):
|
| 2 |
+
w_fn, w_md, w_ml = 0.00, 0.10, 0.90
|
| 3 |
+
mode = "Standard analysis mode"
|
| 4 |
+
mode_detail = "Deep learning models and forensics drive the verdict, metadata is secondary"
|
| 5 |
+
|
| 6 |
+
def share(a, r):
|
| 7 |
+
t = a + r
|
| 8 |
+
if t == 0:
|
| 9 |
+
return 50.0, 50.0
|
| 10 |
+
return (a / t) * 100, (r / t) * 100
|
| 11 |
+
|
| 12 |
+
def share_md(a, r):
|
| 13 |
+
# Smoothing baseline to prevent small metadata points from dominating
|
| 14 |
+
baseline = 30.0
|
| 15 |
+
t = a + r + baseline
|
| 16 |
+
return ((a + baseline / 2) / t) * 100, ((r + baseline / 2) / t) * 100
|
| 17 |
+
|
| 18 |
+
fn_a, fn_r = 0.0, 100.0
|
| 19 |
+
md_a, md_r = share_md(md.get("ai_points", 0), md.get("real_points", 0))
|
| 20 |
+
|
| 21 |
+
# Incorporate Visual Style & Symmetry analysis
|
| 22 |
+
st = ml.get("style", {})
|
| 23 |
+
st_ai_points = st.get("ai_points", 0)
|
| 24 |
+
st_real_points = st.get("real_points", 0)
|
| 25 |
+
|
| 26 |
+
def share_st(a, r):
|
| 27 |
+
baseline = 20.0
|
| 28 |
+
t = a + r + baseline
|
| 29 |
+
return ((a + baseline / 2) / t) * 100, ((r + baseline / 2) / t) * 100
|
| 30 |
+
|
| 31 |
+
st_a, st_r = share_st(st_ai_points, st_real_points)
|
| 32 |
+
base_ml_a, base_ml_r = share(ml.get("ai_points", 0), ml.get("real_points", 0))
|
| 33 |
+
|
| 34 |
+
# Grouped Models/Forensics layer combines models (90%) and style analysis (10%)
|
| 35 |
+
ml_a = base_ml_a * 0.90 + st_a * 0.10
|
| 36 |
+
ml_r = base_ml_r * 0.90 + st_r * 0.10
|
| 37 |
+
|
| 38 |
+
ai_s = fn_a * w_fn + md_a * w_md + ml_a * w_ml
|
| 39 |
+
real_s = fn_r * w_fn + md_r * w_md + ml_r * w_ml
|
| 40 |
+
tot = ai_s + real_s or 1
|
| 41 |
+
ai_s = round((ai_s / tot) * 100, 1)
|
| 42 |
+
real_s = round((real_s / tot) * 100, 1)
|
| 43 |
+
|
| 44 |
+
forensic_override = False
|
| 45 |
+
forensics = ml.get("forensics", {})
|
| 46 |
+
if forensics:
|
| 47 |
+
kurt_ai = forensics.get("kurtosis", {}).get("ai_prob", 0.5)
|
| 48 |
+
dfi_ai = forensics.get("dfi", {}).get("ai_prob", 0.5)
|
| 49 |
+
model_ai = ml.get("weighted_ai_prob", 0.5)
|
| 50 |
+
forensic_avg = (kurt_ai + dfi_ai) / 2
|
| 51 |
+
if abs(forensic_avg - model_ai) > 0.40:
|
| 52 |
+
forensic_override = True
|
| 53 |
+
|
| 54 |
+
if ai_s >= 50:
|
| 55 |
+
verdict = "Fake"
|
| 56 |
+
if ai_s >= 85:
|
| 57 |
+
confidence = "Very High"
|
| 58 |
+
color = "#ef4444"
|
| 59 |
+
elif ai_s >= 70:
|
| 60 |
+
confidence = "High"
|
| 61 |
+
color = "#f87171"
|
| 62 |
+
else:
|
| 63 |
+
confidence = "Medium"
|
| 64 |
+
color = "#f97316"
|
| 65 |
+
else:
|
| 66 |
+
verdict = "Real"
|
| 67 |
+
if ai_s <= 15:
|
| 68 |
+
confidence = "Very High"
|
| 69 |
+
color = "#22c55e"
|
| 70 |
+
elif ai_s <= 30:
|
| 71 |
+
confidence = "High"
|
| 72 |
+
color = "#4ade80"
|
| 73 |
+
else:
|
| 74 |
+
confidence = "Medium"
|
| 75 |
+
color = "#86efac"
|
| 76 |
+
|
| 77 |
+
if forensic_override:
|
| 78 |
+
if confidence in ("Very High", "High"):
|
| 79 |
+
confidence = "Medium"
|
| 80 |
+
elif confidence == "Medium":
|
| 81 |
+
confidence = "Low"
|
| 82 |
+
|
| 83 |
+
breakdown = [
|
| 84 |
+
{
|
| 85 |
+
"layer": "Filename Analysis",
|
| 86 |
+
"ai_pts": 0,
|
| 87 |
+
"real_pts": 0,
|
| 88 |
+
"weight_pct": "0%",
|
| 89 |
+
"signals": ["Filename analysis disabled."],
|
| 90 |
+
"mode": "Layer deactivated.",
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"layer": "Metadata Analysis",
|
| 94 |
+
"ai_pts": md.get("ai_points", 0),
|
| 95 |
+
"real_pts": md.get("real_points", 0),
|
| 96 |
+
"weight_pct": f"{int(w_md * 100)}%",
|
| 97 |
+
"signals": md.get("signals", []),
|
| 98 |
+
"mode": "Metadata-provenance layer.",
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"layer": "AI Model and Forensic Detectors",
|
| 102 |
+
"ai_pts": int(ml_a),
|
| 103 |
+
"real_pts": int(ml_r),
|
| 104 |
+
"weight_pct": f"{int(w_ml * 100)}%",
|
| 105 |
+
"signals": ml.get("signals", []),
|
| 106 |
+
"votes": ml.get("votes", []),
|
| 107 |
+
"forensics": ml.get("forensics", {}),
|
| 108 |
+
"mode": ml.get("priority_note", "") + " With visual style & symmetry checks.",
|
| 109 |
+
},
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
summary = (
|
| 113 |
+
f"Scoring mode: {mode}. "
|
| 114 |
+
f"Final AI score: {ai_s}%. "
|
| 115 |
+
f"Verdict: {verdict} (Confidence: {confidence}). "
|
| 116 |
+
f"{mode_detail}."
|
| 117 |
+
)
|
| 118 |
+
if forensic_override:
|
| 119 |
+
summary += " Forensic signals conflict with model predictions."
|
| 120 |
+
|
| 121 |
+
return {
|
| 122 |
+
"verdict": verdict,
|
| 123 |
+
"ai_score": ai_s,
|
| 124 |
+
"real_score": real_s,
|
| 125 |
+
"confidence": confidence,
|
| 126 |
+
"color": color,
|
| 127 |
+
"breakdown": breakdown,
|
| 128 |
+
"summary": summary,
|
| 129 |
+
"scoring_mode": mode,
|
| 130 |
+
"forensic_override": forensic_override,
|
| 131 |
+
"weights": {"filename": w_fn, "metadata": w_md, "models": w_ml},
|
| 132 |
+
}
|
backend/detectors/style_detector.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
|
| 5 |
+
def analyze_visual_style(img: Image.Image) -> dict:
|
| 6 |
+
signals = []
|
| 7 |
+
ai_pts = 0
|
| 8 |
+
real_pts = 0
|
| 9 |
+
correlation = 0.5
|
| 10 |
+
lap_var = 100.0
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
# Convert PIL Image to grayscale numpy array
|
| 14 |
+
img_rgb = np.array(img.convert("RGB"))
|
| 15 |
+
gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
|
| 16 |
+
|
| 17 |
+
# 1. Symmetry Analysis
|
| 18 |
+
# AI images (especially portraits or generated objects) often exhibit unnatural bilateral symmetry
|
| 19 |
+
h, w = gray.shape
|
| 20 |
+
size = min(h, w, 256)
|
| 21 |
+
resized = cv2.resize(gray, (size, size))
|
| 22 |
+
flipped = cv2.flip(resized, 1) # Horizontal flip
|
| 23 |
+
|
| 24 |
+
# Calculate Pearson correlation coefficient between original and flipped halves, checking std first
|
| 25 |
+
std_val = np.std(resized)
|
| 26 |
+
if std_val > 1e-4:
|
| 27 |
+
corr_matrix = np.corrcoef(resized.flat, flipped.flat)
|
| 28 |
+
if corr_matrix.shape == (2, 2):
|
| 29 |
+
correlation = float(corr_matrix[0, 1])
|
| 30 |
+
if np.isnan(correlation):
|
| 31 |
+
correlation = 0.5
|
| 32 |
+
|
| 33 |
+
if correlation > 0.94:
|
| 34 |
+
ai_pts += 15
|
| 35 |
+
signals.append(f"[STYLE] Unnaturally high horizontal symmetry (corr={correlation:.3f}) (+15 AI)")
|
| 36 |
+
elif correlation > 0.88:
|
| 37 |
+
ai_pts += 5
|
| 38 |
+
signals.append(f"[STYLE] Moderate horizontal symmetry (corr={correlation:.3f}) (+5 AI)")
|
| 39 |
+
elif correlation < 0.35:
|
| 40 |
+
real_pts += 10
|
| 41 |
+
signals.append(f"[STYLE] Natural asymmetry (corr={correlation:.3f}) (+10 Real)")
|
| 42 |
+
|
| 43 |
+
# 2. Over-smoothing / Blur Detection
|
| 44 |
+
# AI textures are often unnaturally smooth or lack fine, high-frequency camera noise
|
| 45 |
+
lap_var = float(cv2.Laplacian(gray, cv2.CV_64F).var())
|
| 46 |
+
|
| 47 |
+
if lap_var < 50.0:
|
| 48 |
+
ai_pts += 12
|
| 49 |
+
signals.append(f"[STYLE] Extremely smooth texture / low local contrast (var={lap_var:.2f}) (+12 AI)")
|
| 50 |
+
elif lap_var < 150.0:
|
| 51 |
+
ai_pts += 6
|
| 52 |
+
signals.append(f"[STYLE] Soft textures / over-smoothing (var={lap_var:.2f}) (+6 AI)")
|
| 53 |
+
elif lap_var > 600.0:
|
| 54 |
+
real_pts += 10
|
| 55 |
+
signals.append(f"[STYLE] High-frequency natural textures (var={lap_var:.2f}) (+10 Real)")
|
| 56 |
+
|
| 57 |
+
except Exception as e:
|
| 58 |
+
signals.append(f"[ERROR] Style analysis error: {str(e)[:60]}")
|
| 59 |
+
|
| 60 |
+
return {
|
| 61 |
+
"ai_points": min(ai_pts, 30),
|
| 62 |
+
"real_points": min(real_pts, 20),
|
| 63 |
+
"signals": signals if signals else ["No style anomalies detected."],
|
| 64 |
+
"symmetry_correlation": round(correlation, 4),
|
| 65 |
+
"texture_variance": round(lap_var, 2),
|
| 66 |
+
}
|
backend/main.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 2 |
+
from fastapi.staticfiles import StaticFiles
|
| 3 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 4 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
import uvicorn
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from backend.detectors.image_detector import analyze_image_models
|
| 9 |
+
from backend.detectors.metadata_detector import analyze_metadata
|
| 10 |
+
from backend.detectors.scoring_engine import calculate_final_score
|
| 11 |
+
from backend.detectors.style_detector import analyze_visual_style
|
| 12 |
+
from PIL import Image
|
| 13 |
+
import io
|
| 14 |
+
|
| 15 |
+
app = FastAPI(title="ImgAuth AI API")
|
| 16 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
| 17 |
+
|
| 18 |
+
BASE_DIR = Path(__file__).parent
|
| 19 |
+
FRONTEND = BASE_DIR.parent / "frontend"
|
| 20 |
+
PUBLIC = BASE_DIR.parent / "public"
|
| 21 |
+
app.mount("/static", StaticFiles(directory=str(FRONTEND / "static")), name="static")
|
| 22 |
+
app.mount("/public", StaticFiles(directory=str(PUBLIC)), name="public")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@app.on_event("startup")
|
| 26 |
+
async def startup():
|
| 27 |
+
print("ImgAuth AI started -> http://localhost:5000")
|
| 28 |
+
try:
|
| 29 |
+
print("[ImgAuth] Preloading models on startup...")
|
| 30 |
+
from backend.detectors.image_detector import load_models
|
| 31 |
+
load_models()
|
| 32 |
+
print("[ImgAuth] Models preloaded successfully.")
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"[ImgAuth] Warning: failed to preload models: {e}")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@app.get("/")
|
| 38 |
+
async def root():
|
| 39 |
+
return FileResponse(str(FRONTEND / "index.html"))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@app.post("/api/detect")
|
| 43 |
+
async def detect(file: UploadFile = File(...)):
|
| 44 |
+
if not file.content_type.startswith("image/"):
|
| 45 |
+
raise HTTPException(400, "Only image files are accepted.")
|
| 46 |
+
contents = await file.read()
|
| 47 |
+
if len(contents) > 10 * 1024 * 1024:
|
| 48 |
+
raise HTTPException(400, "File too large. Max 10 MB.")
|
| 49 |
+
|
| 50 |
+
fn = {}
|
| 51 |
+
md = analyze_metadata(contents)
|
| 52 |
+
ml = analyze_image_models(contents)
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
img_full = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 56 |
+
st = analyze_visual_style(img_full)
|
| 57 |
+
except Exception as e:
|
| 58 |
+
st = {"ai_points": 0, "real_points": 0, "signals": [f"[ERROR] Style loading: {str(e)[:50]}"]}
|
| 59 |
+
|
| 60 |
+
ml["style"] = st
|
| 61 |
+
if "signals" in ml and "signals" in st:
|
| 62 |
+
ml["signals"].extend(st["signals"])
|
| 63 |
+
|
| 64 |
+
res = calculate_final_score(fn, md, ml)
|
| 65 |
+
|
| 66 |
+
return JSONResponse({
|
| 67 |
+
"filename": file.filename,
|
| 68 |
+
"verdict": res["verdict"],
|
| 69 |
+
"ai_score": res["ai_score"],
|
| 70 |
+
"real_score": res["real_score"],
|
| 71 |
+
"confidence": res["confidence"],
|
| 72 |
+
"color": res["color"],
|
| 73 |
+
"layers": {"filename": fn, "metadata": md, "models": ml},
|
| 74 |
+
"breakdown": res["breakdown"],
|
| 75 |
+
"summary": res["summary"],
|
| 76 |
+
"timestamp": datetime.now().isoformat()
|
| 77 |
+
})
|
frontend/index.html
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>ImgAuth AI — Check if an Image is AI-Generated</title>
|
| 7 |
+
<meta name="description" content="Free AI image authenticity checker. Upload any image to instantly find out if it is real or AI-generated. Private, fast, no account needed." />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
|
| 11 |
+
<link rel="stylesheet" href="/static/css/style.css" />
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
|
| 15 |
+
<!-- ── HEADER ─────────────────────────────────────────────────── -->
|
| 16 |
+
<header class="header">
|
| 17 |
+
<div class="header-inner">
|
| 18 |
+
<a class="logo" href="/">
|
| 19 |
+
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
|
| 20 |
+
<rect width="32" height="32" rx="8" fill="url(#lg1)"/>
|
| 21 |
+
<path d="M8 22L13 12L17 19L20 15L24 22H8Z" fill="white" opacity="0.9"/>
|
| 22 |
+
<circle cx="22" cy="11" r="3" fill="white" opacity="0.75"/>
|
| 23 |
+
<defs>
|
| 24 |
+
<linearGradient id="lg1" x1="0" y1="0" x2="32" y2="32">
|
| 25 |
+
<stop stop-color="#7c3aed"/><stop offset="1" stop-color="#a855f7"/>
|
| 26 |
+
</linearGradient>
|
| 27 |
+
</defs>
|
| 28 |
+
</svg>
|
| 29 |
+
<span>ImgAuth <span class="logo-accent">AI</span></span>
|
| 30 |
+
</a>
|
| 31 |
+
<nav class="nav-links">
|
| 32 |
+
<a href="#how-it-works" class="nav-link">How it works</a>
|
| 33 |
+
<a href="#about" class="nav-link">About</a>
|
| 34 |
+
<a href="#team" class="nav-link">Team</a>
|
| 35 |
+
<button class="theme-btn" data-theme-toggle aria-label="Toggle theme">🌙</button>
|
| 36 |
+
</nav>
|
| 37 |
+
</div>
|
| 38 |
+
</header>
|
| 39 |
+
|
| 40 |
+
<!-- ── HERO ───────────────────────────────────────────────────── -->
|
| 41 |
+
<section class="hero">
|
| 42 |
+
<div class="hero-eyebrow">
|
| 43 |
+
<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5" fill="currentColor"/></svg>
|
| 44 |
+
AI Image Authenticity Checker
|
| 45 |
+
</div>
|
| 46 |
+
<h1>Check if an image is<br><span class="grad">AI-generated</span></h1>
|
| 47 |
+
<p class="hero-sub">Upload any image and our AI analyzes visual patterns, metadata, and forensic signals to estimate whether it was AI-generated or authentic.</p>
|
| 48 |
+
<div class="hero-stats">
|
| 49 |
+
<div class="stat"><div class="stat-num">3+</div><div class="stat-label">AI Models</div></div>
|
| 50 |
+
<div class="stat"><div class="stat-num">5+</div><div class="stat-label">Forensic Signals</div></div>
|
| 51 |
+
<div class="stat"><div class="stat-num">100%</div><div class="stat-label">Private</div></div>
|
| 52 |
+
</div>
|
| 53 |
+
</section>
|
| 54 |
+
|
| 55 |
+
<!-- ── UPLOAD ─────────────────────────────────────────────────── -->
|
| 56 |
+
<section class="upload-section" id="uploadSection">
|
| 57 |
+
<div class="upload-card" id="uploadCard">
|
| 58 |
+
<!-- Drop Zone -->
|
| 59 |
+
<div class="drop-zone" id="dropZone">
|
| 60 |
+
<div class="drop-icon">
|
| 61 |
+
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 62 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
| 63 |
+
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
| 64 |
+
</svg>
|
| 65 |
+
</div>
|
| 66 |
+
<p class="drop-title">Drop your image here</p>
|
| 67 |
+
<p class="drop-sub">PNG, JPG, or WEBP · Max 10 MB</p>
|
| 68 |
+
<label class="btn btn-primary" for="fileInput">Select Image</label>
|
| 69 |
+
<input type="file" id="fileInput" accept="image/*" hidden />
|
| 70 |
+
<p class="drop-hint">or drag and drop</p>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<!-- Preview Zone -->
|
| 74 |
+
<div class="preview-zone" id="previewZone" style="display:none">
|
| 75 |
+
<div class="preview-img-box">
|
| 76 |
+
<img id="previewImg" src="" alt="Selected image preview" />
|
| 77 |
+
</div>
|
| 78 |
+
<div class="preview-filename" id="previewFilename">—</div>
|
| 79 |
+
<div class="preview-actions">
|
| 80 |
+
<button class="btn btn-primary" id="analyzeBtn">
|
| 81 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
| 82 |
+
Check Image
|
| 83 |
+
</button>
|
| 84 |
+
<button class="btn btn-ghost" id="clearBtn">Choose Different</button>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</section>
|
| 89 |
+
|
| 90 |
+
<!-- ── LOADING OVERLAY ────────────────────────────────────────── -->
|
| 91 |
+
<div class="loading-overlay" id="loadingOverlay" style="display:none">
|
| 92 |
+
<div class="loading-card">
|
| 93 |
+
<div class="spinner-ring"></div>
|
| 94 |
+
<p class="loading-title">Analyzing your image</p>
|
| 95 |
+
<p class="loading-sub">This usually takes 10–30 seconds</p>
|
| 96 |
+
<div class="steps-list">
|
| 97 |
+
<div class="step" id="step1">Reading image metadata</div>
|
| 98 |
+
<div class="step" id="step2">Analyzing visual textures</div>
|
| 99 |
+
<div class="step" id="step3">Running AI detection models</div>
|
| 100 |
+
<div class="step" id="step4">Checking camera fingerprint</div>
|
| 101 |
+
<div class="step" id="step5">Mapping pixel irregularities</div>
|
| 102 |
+
<div class="step" id="step6">Calculating authenticity score</div>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<!-- ── RESULT SECTION ─────────────────────────────────────────── -->
|
| 108 |
+
<section class="result-section" id="resultSection" style="display:none">
|
| 109 |
+
<div class="result-wrap">
|
| 110 |
+
|
| 111 |
+
<!-- Verdict Card -->
|
| 112 |
+
<div class="verdict-card">
|
| 113 |
+
<div class="verdict-layout">
|
| 114 |
+
<div class="result-img-panel">
|
| 115 |
+
<img id="resultImg" src="" alt="Analyzed image" />
|
| 116 |
+
</div>
|
| 117 |
+
<div class="verdict-info">
|
| 118 |
+
<!-- Badge + Confidence -->
|
| 119 |
+
<div>
|
| 120 |
+
<div class="verdict-badge" id="resultBadge">—</div>
|
| 121 |
+
</div>
|
| 122 |
+
<h2 class="verdict-heading" id="resultVerdict">—</h2>
|
| 123 |
+
<p class="confidence-label" id="resultConfidenceLevel">—</p>
|
| 124 |
+
|
| 125 |
+
<!-- Gauge Bar -->
|
| 126 |
+
<div class="gauge-wrap">
|
| 127 |
+
<div class="gauge-meta">
|
| 128 |
+
<span>Likely Authentic</span>
|
| 129 |
+
<span>Likely AI-Generated</span>
|
| 130 |
+
</div>
|
| 131 |
+
<div class="gauge-track">
|
| 132 |
+
<div class="gauge-fill" id="gaugeFill" style="width:0%"></div>
|
| 133 |
+
<div class="gauge-dot" id="gaugeDot" style="left:0%"></div>
|
| 134 |
+
</div>
|
| 135 |
+
<div class="gauge-vals">
|
| 136 |
+
<span id="realPct">—</span>
|
| 137 |
+
<span id="aiPct">—</span>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<!-- Summary -->
|
| 142 |
+
<p class="verdict-summary-text" id="resultSummary">—</p>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<!-- Why This Result -->
|
| 148 |
+
<div>
|
| 149 |
+
<p class="result-block-title">Why this result?</p>
|
| 150 |
+
<div class="why-grid">
|
| 151 |
+
<div class="why-card">
|
| 152 |
+
<div class="why-icon purple">🎨</div>
|
| 153 |
+
<h4>Visual Patterns</h4>
|
| 154 |
+
<p id="explainVisual">—</p>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="why-card">
|
| 157 |
+
<div class="why-icon blue">🤖</div>
|
| 158 |
+
<h4>AI Model Analysis</h4>
|
| 159 |
+
<p id="explainModels">—</p>
|
| 160 |
+
</div>
|
| 161 |
+
<div class="why-card">
|
| 162 |
+
<div class="why-icon teal">📷</div>
|
| 163 |
+
<h4>Metadata Check</h4>
|
| 164 |
+
<p id="explainMetadata">—</p>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<!-- AI Focus Areas -->
|
| 170 |
+
<div id="focusAreas" style="display:none">
|
| 171 |
+
<p class="result-block-title">AI Focus Areas</p>
|
| 172 |
+
<div class="focus-grid">
|
| 173 |
+
<div class="focus-card">
|
| 174 |
+
<h4>AI Attention Areas</h4>
|
| 175 |
+
<p>Where our models focused most during analysis.</p>
|
| 176 |
+
<div class="heatmap-box">
|
| 177 |
+
<img id="attentionHeatmapImg" src="" alt="AI Attention Map" />
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
<div class="focus-card" id="dfiCardContainer" style="display:none">
|
| 181 |
+
<h4>Pattern Irregularities</h4>
|
| 182 |
+
<p>Texture inconsistencies detected in pixel patterns.</p>
|
| 183 |
+
<div class="heatmap-box">
|
| 184 |
+
<img id="dfiHeatmapImg" src="" alt="Pattern Irregularities" />
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<!-- Advanced Technical Analysis (Collapsed) -->
|
| 191 |
+
<div class="verdict-card adv-section" id="advancedDetails">
|
| 192 |
+
<button class="adv-toggle" id="advancedToggle" aria-expanded="false">
|
| 193 |
+
<span>Show Technical Analysis</span>
|
| 194 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
| 195 |
+
<polyline points="6 9 12 15 18 9"/>
|
| 196 |
+
</svg>
|
| 197 |
+
</button>
|
| 198 |
+
<div class="adv-body" id="advancedBody" style="display:none">
|
| 199 |
+
<div class="adv-grid" id="advancedContent"></div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<!-- CTA -->
|
| 204 |
+
<div class="result-cta">
|
| 205 |
+
<button class="btn btn-primary" id="newScanBtn">Check Another Image</button>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
</div>
|
| 209 |
+
</section>
|
| 210 |
+
|
| 211 |
+
<!-- ── HOW IT WORKS ────────────────────────────────────────────── -->
|
| 212 |
+
<section class="how-section" id="how-it-works">
|
| 213 |
+
<div class="page-wrap">
|
| 214 |
+
<div class="section-header">
|
| 215 |
+
<div class="section-tag">Simple Process</div>
|
| 216 |
+
<h2 class="section-title">How It Works</h2>
|
| 217 |
+
<p class="section-desc">Three simple steps to check any image in seconds.</p>
|
| 218 |
+
</div>
|
| 219 |
+
<div class="how-grid">
|
| 220 |
+
<div class="how-card">
|
| 221 |
+
<div class="how-num">01</div>
|
| 222 |
+
<h3>Upload an image</h3>
|
| 223 |
+
<p>PNG, JPG, or WEBP. Your image is processed instantly in memory and never stored on our servers.</p>
|
| 224 |
+
</div>
|
| 225 |
+
<div class="how-card">
|
| 226 |
+
<div class="how-num">02</div>
|
| 227 |
+
<h3>AI analyzes patterns</h3>
|
| 228 |
+
<p>We run multiple deep learning models and forensic checks on visual textures, metadata, and pixel patterns.</p>
|
| 229 |
+
</div>
|
| 230 |
+
<div class="how-card">
|
| 231 |
+
<div class="how-num">03</div>
|
| 232 |
+
<h3>Get your estimate</h3>
|
| 233 |
+
<p>Receive a clear authenticity score with simple explanations of why the result was reached.</p>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
</section>
|
| 238 |
+
|
| 239 |
+
<!-- ── ABOUT ───────────────────────────────────────────────────── -->
|
| 240 |
+
<section class="about-section" id="about">
|
| 241 |
+
<div class="page-wrap">
|
| 242 |
+
<div class="about-grid">
|
| 243 |
+
<div class="about-text">
|
| 244 |
+
<div class="section-tag">College Major Project</div>
|
| 245 |
+
<h2>About ImgAuth AI</h2>
|
| 246 |
+
<p>ImgAuth AI is an academic AI and cybersecurity project developed to detect AI-generated or manipulated images using deep learning and forensic analysis techniques.</p>
|
| 247 |
+
<p>The platform combines multiple state-of-the-art AI models and forensic signals to estimate whether an image may be synthetic or authentic — making cutting-edge detection accessible to everyone.</p>
|
| 248 |
+
<p>Built as a college major project focused on AI safety, digital trust, and media authenticity in an age of rapidly advancing generative AI.</p>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="about-visual">
|
| 251 |
+
<div class="about-stat-grid">
|
| 252 |
+
<div class="about-stat">
|
| 253 |
+
<div class="about-stat-num">3</div>
|
| 254 |
+
<div class="about-stat-label">AI Detection Models</div>
|
| 255 |
+
</div>
|
| 256 |
+
<div class="about-stat">
|
| 257 |
+
<div class="about-stat-num">5+</div>
|
| 258 |
+
<div class="about-stat-label">Forensic Analyzers</div>
|
| 259 |
+
</div>
|
| 260 |
+
<div class="about-stat">
|
| 261 |
+
<div class="about-stat-num">0</div>
|
| 262 |
+
<div class="about-stat-label">Images Stored</div>
|
| 263 |
+
</div>
|
| 264 |
+
<div class="about-stat">
|
| 265 |
+
<div class="about-stat-num">4</div>
|
| 266 |
+
<div class="about-stat-label">Team Members</div>
|
| 267 |
+
</div>
|
| 268 |
+
</div>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
</div>
|
| 272 |
+
</section>
|
| 273 |
+
|
| 274 |
+
<!-- ── PROJECT HIGHLIGHTS ─────────────────────────────────────── -->
|
| 275 |
+
<section class="highlights-section" id="highlights">
|
| 276 |
+
<div class="page-wrap">
|
| 277 |
+
<div class="section-header">
|
| 278 |
+
<div class="section-tag">Capabilities</div>
|
| 279 |
+
<h2 class="section-title">Project Highlights</h2>
|
| 280 |
+
<p class="section-desc">What makes ImgAuth AI powerful under the hood.</p>
|
| 281 |
+
</div>
|
| 282 |
+
<div class="highlights-grid">
|
| 283 |
+
<div class="highlight-card">
|
| 284 |
+
<div class="highlight-icon">🧠</div>
|
| 285 |
+
<h4>Multi-Model AI Detection</h4>
|
| 286 |
+
<p>Three independent deep learning models vote on whether an image is AI-generated, increasing accuracy and reducing bias.</p>
|
| 287 |
+
</div>
|
| 288 |
+
<div class="highlight-card">
|
| 289 |
+
<div class="highlight-icon">🔬</div>
|
| 290 |
+
<h4>Visual Forensic Analysis</h4>
|
| 291 |
+
<p>Noise kurtosis, FFT spectral analysis, and deep feature inconsistency mapping expose subtle AI generation artifacts.</p>
|
| 292 |
+
</div>
|
| 293 |
+
<div class="highlight-card">
|
| 294 |
+
<div class="highlight-icon">🗺️</div>
|
| 295 |
+
<h4>Explainable AI Visualization</h4>
|
| 296 |
+
<p>Heatmaps and attention maps highlight exactly which regions of the image influenced the detection result.</p>
|
| 297 |
+
</div>
|
| 298 |
+
<div class="highlight-card">
|
| 299 |
+
<div class="highlight-icon">🔒</div>
|
| 300 |
+
<h4>Privacy-Focused Processing</h4>
|
| 301 |
+
<p>All analysis happens in-memory. Images are never written to disk, stored in databases, or shared with third parties.</p>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="highlight-card">
|
| 304 |
+
<div class="highlight-icon">⚡</div>
|
| 305 |
+
<h4>Real-Time Image Scanning</h4>
|
| 306 |
+
<p>Fast pipeline design minimizes model load time. Results are typically delivered within 10–30 seconds.</p>
|
| 307 |
+
</div>
|
| 308 |
+
<div class="highlight-card">
|
| 309 |
+
<div class="highlight-icon">📊</div>
|
| 310 |
+
<h4>Confidence Scoring System</h4>
|
| 311 |
+
<p>A weighted ensemble scoring engine combines all signals into a single, calibrated authenticity confidence score.</p>
|
| 312 |
+
</div>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</section>
|
| 316 |
+
|
| 317 |
+
<!-- ── TECHNOLOGY STACK ────────────────────────────────────────── -->
|
| 318 |
+
<section class="tech-section" id="tech">
|
| 319 |
+
<div class="page-wrap">
|
| 320 |
+
<div class="section-header">
|
| 321 |
+
<div class="section-tag">Built With</div>
|
| 322 |
+
<h2 class="section-title">Technology Stack</h2>
|
| 323 |
+
<p class="section-desc">Modern tools and frameworks powering the platform.</p>
|
| 324 |
+
</div>
|
| 325 |
+
<div class="tech-grid">
|
| 326 |
+
<div class="tech-group">
|
| 327 |
+
<div class="tech-group-label">Frontend</div>
|
| 328 |
+
<div class="tech-chips">
|
| 329 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#e34c26"></span>HTML5</span>
|
| 330 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#264de4"></span>CSS3</span>
|
| 331 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#f7df1e"></span>JavaScript</span>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
<div class="tech-group">
|
| 335 |
+
<div class="tech-group-label">Backend</div>
|
| 336 |
+
<div class="tech-chips">
|
| 337 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#009688"></span>FastAPI</span>
|
| 338 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#3776ab"></span>Python 3.11</span>
|
| 339 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#10b981"></span>Uvicorn</span>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
<div class="tech-group">
|
| 343 |
+
<div class="tech-group-label">AI / ML</div>
|
| 344 |
+
<div class="tech-chips">
|
| 345 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#ee4c2c"></span>PyTorch</span>
|
| 346 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#f59e0b"></span>HuggingFace</span>
|
| 347 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#7c3aed"></span>ViT Model</span>
|
| 348 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#a855f7"></span>CNN Model</span>
|
| 349 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#06b6d4"></span>OpenCV</span>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
<div class="tech-group">
|
| 353 |
+
<div class="tech-group-label">Deployment</div>
|
| 354 |
+
<div class="tech-chips">
|
| 355 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#ff6b35"></span>HuggingFace Spaces</span>
|
| 356 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#24292e"></span>Docker</span>
|
| 357 |
+
<span class="tech-chip"><span class="tech-chip-dot" style="background:#2563eb"></span>GitHub</span>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
</section>
|
| 363 |
+
|
| 364 |
+
<!-- ── MEET THE TEAM ───────────────────────────────────────────── -->
|
| 365 |
+
<section class="team-section" id="team">
|
| 366 |
+
<div class="page-wrap">
|
| 367 |
+
<div class="section-header">
|
| 368 |
+
<div class="section-tag">The Builders</div>
|
| 369 |
+
<h2 class="section-title">Meet the Team</h2>
|
| 370 |
+
<p class="section-desc">Four students building at the intersection of AI, cybersecurity, and design.</p>
|
| 371 |
+
</div>
|
| 372 |
+
<div class="team-grid">
|
| 373 |
+
|
| 374 |
+
<!-- Vishal -->
|
| 375 |
+
<div class="team-card">
|
| 376 |
+
<div class="team-avatar team-avatar--photo" style="background-image:url('https://raw.githubusercontent.com/Raksha-dev11/ImgAuth/main/frontend/static/images/vishal.jpeg'); background-size: 90%; background-position: center 15%;"></div>
|
| 377 |
+
<div class="team-name">Vishal Chauhan</div>
|
| 378 |
+
<div class="team-role">Project Lead & Detection Logic</div>
|
| 379 |
+
<div class="team-branch">CCS</div>
|
| 380 |
+
<p class="team-desc">Designed overall architecture, coordinated module integration, defined the scoring strategy, and supervised technical direction and core detector logic.</p>
|
| 381 |
+
<div class="team-links">
|
| 382 |
+
<a href="https://github.com/metaexploder" class="team-link" aria-label="GitHub" target="_blank" rel="noopener noreferrer">
|
| 383 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>
|
| 384 |
+
</a>
|
| 385 |
+
<a href="https://www.linkedin.com/in/vishal8448" class="team-link" aria-label="LinkedIn" target="_blank" rel="noopener noreferrer">
|
| 386 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
| 387 |
+
</a>
|
| 388 |
+
</div>
|
| 389 |
+
</div>
|
| 390 |
+
|
| 391 |
+
<!-- Prince Mishra -->
|
| 392 |
+
<div class="team-card">
|
| 393 |
+
<div class="team-avatar team-avatar--photo" style="background-image:url('https://raw.githubusercontent.com/Raksha-dev11/ImgAuth/main/frontend/static/images/princemishra.jpeg')"></div>
|
| 394 |
+
<div class="team-name">Prince Mishra</div>
|
| 395 |
+
<div class="team-role">Backend & API Developer</div>
|
| 396 |
+
<div class="team-branch">CSE</div>
|
| 397 |
+
<p class="team-desc">Implemented FastAPI routes, file validation, request handling, backend orchestration, and API-result communication between all system layers.</p>
|
| 398 |
+
<div class="team-links">
|
| 399 |
+
<a href="https://github.com/Prince1605" class="team-link" aria-label="GitHub" target="_blank" rel="noopener noreferrer">
|
| 400 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>
|
| 401 |
+
</a>
|
| 402 |
+
<a href="https://www.linkedin.com/in/prince-mishra-86bb8b343/" class="team-link" aria-label="LinkedIn" target="_blank" rel="noopener noreferrer">
|
| 403 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
| 404 |
+
</a>
|
| 405 |
+
</div>
|
| 406 |
+
</div>
|
| 407 |
+
|
| 408 |
+
<!-- Prince Dubey -->
|
| 409 |
+
<div class="team-card">
|
| 410 |
+
<div class="team-avatar team-avatar--photo" style="background-image:url('https://raw.githubusercontent.com/Raksha-dev11/ImgAuth/main/frontend/static/images/princedubey.jpeg')"></div>
|
| 411 |
+
<div class="team-name">Prince Dubey</div>
|
| 412 |
+
<div class="team-role">Security, Testing & Docs</div>
|
| 413 |
+
<div class="team-branch">CSE</div>
|
| 414 |
+
<p class="team-desc">Managed QA workflows, testing strategies, bug tracking, documentation, and security review of file handling and user interactions.</p>
|
| 415 |
+
<div class="team-links">
|
| 416 |
+
<a href="https://github.com/PR1NC3-DUB3Y" class="team-link" aria-label="GitHub" target="_blank" rel="noopener noreferrer">
|
| 417 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>
|
| 418 |
+
</a>
|
| 419 |
+
<a href="https://www.linkedin.com/in/prince-dubey-8a0b65271" class="team-link" aria-label="LinkedIn" target="_blank" rel="noopener noreferrer">
|
| 420 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
| 421 |
+
</a>
|
| 422 |
+
</div>
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
<!-- Raksha -->
|
| 426 |
+
<div class="team-card">
|
| 427 |
+
<div class="team-avatar team-avatar--photo" style="background-image:url('https://raw.githubusercontent.com/Raksha-dev11/ImgAuth/main/frontend/static/images/raksha.jpeg'); background-size: 90%; background-position: center 15%;"></div>
|
| 428 |
+
<div class="team-name">Raksha</div>
|
| 429 |
+
<div class="team-role">Frontend & UI Developer</div>
|
| 430 |
+
<div class="team-branch">CSE</div>
|
| 431 |
+
<p class="team-desc">Designed the upload interface, result dashboard, theme system, history display, and client-side interaction logic throughout the application.</p>
|
| 432 |
+
<div class="team-links">
|
| 433 |
+
<a href="https://github.com/Raksha-dev11" class="team-link" aria-label="GitHub" target="_blank" rel="noopener noreferrer">
|
| 434 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>
|
| 435 |
+
</a>
|
| 436 |
+
<a href="https://www.linkedin.com/in/raksha-thakur-ba9316326" class="team-link" aria-label="LinkedIn" target="_blank" rel="noopener noreferrer">
|
| 437 |
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
| 438 |
+
</a>
|
| 439 |
+
</div>
|
| 440 |
+
</div>
|
| 441 |
+
|
| 442 |
+
</div>
|
| 443 |
+
</div>
|
| 444 |
+
</section>
|
| 445 |
+
|
| 446 |
+
<!-- ── FOOTER ──────────────────────────────────────────────────── -->
|
| 447 |
+
<footer class="footer">
|
| 448 |
+
<div class="footer-inner">
|
| 449 |
+
<div class="footer-top">
|
| 450 |
+
<div>
|
| 451 |
+
<div class="footer-brand-name">ImgAuth <span class="logo-accent">AI</span></div>
|
| 452 |
+
<p class="footer-brand-desc">A college AI & cybersecurity project. Detect AI-generated images using deep learning and forensic analysis.</p>
|
| 453 |
+
<p class="footer-privacy-note">🔒 Images are processed in-memory and never permanently stored.</p>
|
| 454 |
+
</div>
|
| 455 |
+
<div class="footer-col">
|
| 456 |
+
<h5>Product</h5>
|
| 457 |
+
<ul>
|
| 458 |
+
<li><a href="#uploadSection">Check an Image</a></li>
|
| 459 |
+
<li><a href="#how-it-works">How it works</a></li>
|
| 460 |
+
<li><a href="#highlights">Features</a></li>
|
| 461 |
+
</ul>
|
| 462 |
+
</div>
|
| 463 |
+
<div class="footer-col">
|
| 464 |
+
<h5>Project</h5>
|
| 465 |
+
<ul>
|
| 466 |
+
<li><a href="#about">About</a></li>
|
| 467 |
+
<li><a href="#tech">Tech Stack</a></li>
|
| 468 |
+
<li><a href="#team">Meet the Team</a></li>
|
| 469 |
+
</ul>
|
| 470 |
+
</div>
|
| 471 |
+
<div class="footer-col">
|
| 472 |
+
<h5>Resources</h5>
|
| 473 |
+
<ul>
|
| 474 |
+
<li><a href="https://github.com" target="_blank" rel="noopener">GitHub</a></li>
|
| 475 |
+
<li><a href="https://huggingface.co/umm-maybe/AI-image-detector" target="_blank" rel="noopener">umm-maybe Model</a></li>
|
| 476 |
+
<li><a href="https://huggingface.co/dima806/ai_vs_real_image_detection" target="_blank" rel="noopener">dima806 Model</a></li>
|
| 477 |
+
</ul>
|
| 478 |
+
</div>
|
| 479 |
+
</div>
|
| 480 |
+
<div class="footer-bottom">
|
| 481 |
+
<p class="footer-copy">© 2026 ImgAuth AI — Built by Team VisionGuard</p>
|
| 482 |
+
<p class="footer-attribution">
|
| 483 |
+
Powered by:
|
| 484 |
+
<a href="https://huggingface.co/umm-maybe/AI-image-detector" target="_blank" rel="noopener">umm-maybe/AI-image-detector</a> ·
|
| 485 |
+
<a href="https://huggingface.co/dima806/ai_vs_real_image_detection" target="_blank" rel="noopener">dima806/ai_vs_real_image_detection</a> ·
|
| 486 |
+
<a href="https://huggingface.co/Organika/sdxl-detector" target="_blank" rel="noopener">Organika/sdxl-detector</a>
|
| 487 |
+
</p>
|
| 488 |
+
</div>
|
| 489 |
+
</div>
|
| 490 |
+
</footer>
|
| 491 |
+
|
| 492 |
+
<script src="/static/js/app.js"></script>
|
| 493 |
+
</body>
|
| 494 |
+
</html>
|
frontend/static/css/style.css
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ═══════════════════════════════════════════════════════════════
|
| 2 |
+
ImgAuth AI — Premium Dark UI System
|
| 3 |
+
═══════════════════════════════════════════════════════════════ */
|
| 4 |
+
|
| 5 |
+
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&display=swap');
|
| 6 |
+
|
| 7 |
+
/* ── Design Tokens ─────────────────────────────────────────────── */
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #080810;
|
| 10 |
+
--bg-secondary: #0d0d1a;
|
| 11 |
+
--surface: rgba(255,255,255,0.04);
|
| 12 |
+
--surface-solid: #111122;
|
| 13 |
+
--surface-hover: rgba(255,255,255,0.07);
|
| 14 |
+
--border: rgba(255,255,255,0.08);
|
| 15 |
+
--border-hover: rgba(255,255,255,0.16);
|
| 16 |
+
|
| 17 |
+
--text: #f1f1f8;
|
| 18 |
+
--text-2: #9898b8;
|
| 19 |
+
--text-3: #5a5a7a;
|
| 20 |
+
|
| 21 |
+
--purple: #7c3aed;
|
| 22 |
+
--purple-light: #a855f7;
|
| 23 |
+
--purple-glow: rgba(124,58,237,0.20);
|
| 24 |
+
--purple-grad: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
|
| 25 |
+
|
| 26 |
+
--green: #10b981;
|
| 27 |
+
--green-bg: rgba(16,185,129,0.12);
|
| 28 |
+
--green-border: rgba(16,185,129,0.30);
|
| 29 |
+
--orange: #f59e0b;
|
| 30 |
+
--orange-bg: rgba(245,158,11,0.12);
|
| 31 |
+
--orange-border: rgba(245,158,11,0.30);
|
| 32 |
+
--red: #ef4444;
|
| 33 |
+
--red-bg: rgba(239,68,68,0.12);
|
| 34 |
+
--red-border: rgba(239,68,68,0.30);
|
| 35 |
+
|
| 36 |
+
--radius-xl: 20px;
|
| 37 |
+
--radius-lg: 16px;
|
| 38 |
+
--radius-md: 12px;
|
| 39 |
+
--radius-sm: 8px;
|
| 40 |
+
--radius-xs: 6px;
|
| 41 |
+
|
| 42 |
+
--font: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
|
| 43 |
+
--mono: 'JetBrains Mono', 'Fira Code', monospace;
|
| 44 |
+
|
| 45 |
+
--shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
|
| 46 |
+
--shadow-md: 0 4px 24px rgba(0,0,0,0.4);
|
| 47 |
+
--shadow-lg: 0 8px 48px rgba(0,0,0,0.5);
|
| 48 |
+
--shadow-purple: 0 0 32px rgba(124,58,237,0.25);
|
| 49 |
+
|
| 50 |
+
--transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
[data-theme="light"] {
|
| 54 |
+
--bg: #f5f4fb;
|
| 55 |
+
--bg-secondary: #eeedf8;
|
| 56 |
+
--surface: rgba(255,255,255,0.80);
|
| 57 |
+
--surface-solid: #ffffff;
|
| 58 |
+
--surface-hover: rgba(255,255,255,0.95);
|
| 59 |
+
--border: rgba(0,0,0,0.08);
|
| 60 |
+
--border-hover: rgba(0,0,0,0.16);
|
| 61 |
+
--text: #0d0d1a;
|
| 62 |
+
--text-2: #44445a;
|
| 63 |
+
--text-3: #9898b0;
|
| 64 |
+
--purple-glow: rgba(124,58,237,0.10);
|
| 65 |
+
--shadow-md: 0 4px 24px rgba(0,0,0,0.08);
|
| 66 |
+
--shadow-lg: 0 8px 48px rgba(0,0,0,0.12);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* ── Reset ──────────────────────────────────────────────────────── */
|
| 70 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 71 |
+
html { scroll-behavior: smooth; }
|
| 72 |
+
body {
|
| 73 |
+
background: var(--bg);
|
| 74 |
+
color: var(--text);
|
| 75 |
+
font-family: var(--font);
|
| 76 |
+
line-height: 1.6;
|
| 77 |
+
-webkit-font-smoothing: antialiased;
|
| 78 |
+
-moz-osx-font-smoothing: grayscale;
|
| 79 |
+
overflow-x: hidden;
|
| 80 |
+
}
|
| 81 |
+
a { color: inherit; text-decoration: none; }
|
| 82 |
+
img { max-width: 100%; display: block; }
|
| 83 |
+
button { font-family: var(--font); }
|
| 84 |
+
|
| 85 |
+
/* ── Scrollbar ──────────────────────────────────────────────────── */
|
| 86 |
+
::-webkit-scrollbar { width: 6px; }
|
| 87 |
+
::-webkit-scrollbar-track { background: var(--bg); }
|
| 88 |
+
::-webkit-scrollbar-thumb { background: rgba(124,58,237,0.4); border-radius: 3px; }
|
| 89 |
+
|
| 90 |
+
/* ── Layout Wrappers ────────────────────────────────────────────── */
|
| 91 |
+
.page-wrap { max-width: 1100px; margin: 0 auto; padding: 0 24px; }
|
| 92 |
+
.section-wrap { max-width: 900px; margin: 0 auto; padding: 0 24px; }
|
| 93 |
+
|
| 94 |
+
/* ── Header ─────────────────────────────────────────────────────── */
|
| 95 |
+
.header {
|
| 96 |
+
position: sticky; top: 0; z-index: 200;
|
| 97 |
+
background: rgba(8,8,16,0.80);
|
| 98 |
+
backdrop-filter: blur(20px);
|
| 99 |
+
-webkit-backdrop-filter: blur(20px);
|
| 100 |
+
border-bottom: 1px solid var(--border);
|
| 101 |
+
transition: background var(--transition);
|
| 102 |
+
}
|
| 103 |
+
[data-theme="light"] .header { background: rgba(245,244,251,0.85); }
|
| 104 |
+
|
| 105 |
+
.header-inner {
|
| 106 |
+
max-width: 1200px; margin: 0 auto; padding: 0 24px;
|
| 107 |
+
height: 64px; display: flex; align-items: center; gap: 16px;
|
| 108 |
+
}
|
| 109 |
+
.logo {
|
| 110 |
+
display: flex; align-items: center; gap: 10px;
|
| 111 |
+
font-weight: 800; font-size: 1.1rem; letter-spacing: -0.02em;
|
| 112 |
+
flex-shrink: 0;
|
| 113 |
+
}
|
| 114 |
+
.logo-accent {
|
| 115 |
+
background: var(--purple-grad);
|
| 116 |
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 117 |
+
background-clip: text;
|
| 118 |
+
}
|
| 119 |
+
.nav-links { display: flex; align-items: center; gap: 8px; margin-left: auto; }
|
| 120 |
+
.nav-link {
|
| 121 |
+
color: var(--text-2); font-size: 0.875rem; font-weight: 500;
|
| 122 |
+
padding: 6px 14px; border-radius: var(--radius-sm);
|
| 123 |
+
transition: color var(--transition), background var(--transition);
|
| 124 |
+
}
|
| 125 |
+
.nav-link:hover { color: var(--text); background: var(--surface-hover); }
|
| 126 |
+
.theme-btn {
|
| 127 |
+
width: 38px; height: 38px; border-radius: 50%;
|
| 128 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 129 |
+
color: var(--text-2); cursor: pointer; font-size: 1rem;
|
| 130 |
+
display: flex; align-items: center; justify-content: center;
|
| 131 |
+
transition: var(--transition); flex-shrink: 0;
|
| 132 |
+
}
|
| 133 |
+
.theme-btn:hover { border-color: var(--purple); color: var(--purple); background: var(--purple-glow); }
|
| 134 |
+
|
| 135 |
+
/* ── Buttons ────────────────────────────────────────────────────── */
|
| 136 |
+
.btn {
|
| 137 |
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
| 138 |
+
padding: 12px 28px; border-radius: var(--radius-md);
|
| 139 |
+
font-family: var(--font); font-size: 0.9rem; font-weight: 700;
|
| 140 |
+
border: none; cursor: pointer; transition: var(--transition);
|
| 141 |
+
white-space: nowrap;
|
| 142 |
+
}
|
| 143 |
+
.btn-primary {
|
| 144 |
+
background: var(--purple-grad); color: #fff;
|
| 145 |
+
box-shadow: 0 4px 16px rgba(124,58,237,0.30);
|
| 146 |
+
}
|
| 147 |
+
.btn-primary:hover {
|
| 148 |
+
transform: translateY(-2px);
|
| 149 |
+
box-shadow: 0 8px 28px rgba(124,58,237,0.45);
|
| 150 |
+
}
|
| 151 |
+
.btn-primary:active { transform: translateY(0); }
|
| 152 |
+
.btn-ghost {
|
| 153 |
+
background: transparent; color: var(--text-2);
|
| 154 |
+
border: 1px solid var(--border);
|
| 155 |
+
}
|
| 156 |
+
.btn-ghost:hover { border-color: var(--border-hover); color: var(--text); background: var(--surface-hover); }
|
| 157 |
+
.btn-sm { padding: 8px 18px; font-size: 0.82rem; }
|
| 158 |
+
|
| 159 |
+
/* ── Hero ───────────────────────────────────────────────────────── */
|
| 160 |
+
.hero {
|
| 161 |
+
padding: 96px 24px 64px;
|
| 162 |
+
text-align: center;
|
| 163 |
+
position: relative;
|
| 164 |
+
overflow: hidden;
|
| 165 |
+
}
|
| 166 |
+
.hero::before {
|
| 167 |
+
content: '';
|
| 168 |
+
position: absolute; inset: 0;
|
| 169 |
+
background:
|
| 170 |
+
radial-gradient(ellipse 60% 50% at 50% 0%, rgba(124,58,237,0.18) 0%, transparent 70%);
|
| 171 |
+
pointer-events: none;
|
| 172 |
+
}
|
| 173 |
+
.hero-eyebrow {
|
| 174 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 175 |
+
background: var(--purple-glow); border: 1px solid rgba(124,58,237,0.30);
|
| 176 |
+
color: var(--purple-light); font-size: 0.78rem; font-weight: 700;
|
| 177 |
+
padding: 5px 16px; border-radius: 999px; margin-bottom: 28px;
|
| 178 |
+
letter-spacing: 0.04em; text-transform: uppercase;
|
| 179 |
+
}
|
| 180 |
+
.hero h1 {
|
| 181 |
+
font-size: clamp(2.4rem, 5.5vw, 3.6rem);
|
| 182 |
+
font-weight: 800; line-height: 1.12;
|
| 183 |
+
letter-spacing: -0.03em; margin-bottom: 20px;
|
| 184 |
+
}
|
| 185 |
+
.hero h1 .grad {
|
| 186 |
+
background: var(--purple-grad);
|
| 187 |
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 188 |
+
background-clip: text;
|
| 189 |
+
}
|
| 190 |
+
.hero-sub {
|
| 191 |
+
color: var(--text-2); font-size: 1.05rem; line-height: 1.65;
|
| 192 |
+
max-width: 580px; margin: 0 auto 48px;
|
| 193 |
+
}
|
| 194 |
+
.hero-stats {
|
| 195 |
+
display: flex; justify-content: center; gap: 48px;
|
| 196 |
+
flex-wrap: wrap;
|
| 197 |
+
}
|
| 198 |
+
.stat { text-align: center; }
|
| 199 |
+
.stat-num { font-size: 1.6rem; font-weight: 800; color: var(--purple-light); }
|
| 200 |
+
.stat-label { font-size: 0.78rem; color: var(--text-3); font-weight: 500; }
|
| 201 |
+
|
| 202 |
+
/* ── Upload ─────────────────────────────────────────────────────── */
|
| 203 |
+
.upload-section { padding: 0 24px 80px; }
|
| 204 |
+
.upload-card {
|
| 205 |
+
max-width: 580px; margin: 0 auto;
|
| 206 |
+
background: var(--surface);
|
| 207 |
+
border: 1px solid var(--border);
|
| 208 |
+
border-radius: var(--radius-xl);
|
| 209 |
+
backdrop-filter: blur(12px);
|
| 210 |
+
box-shadow: var(--shadow-md);
|
| 211 |
+
overflow: hidden;
|
| 212 |
+
transition: border-color var(--transition), box-shadow var(--transition);
|
| 213 |
+
}
|
| 214 |
+
.upload-card:hover { border-color: rgba(124,58,237,0.30); box-shadow: var(--shadow-purple); }
|
| 215 |
+
|
| 216 |
+
.drop-zone {
|
| 217 |
+
padding: 52px 32px; text-align: center;
|
| 218 |
+
border: 2px dashed var(--border);
|
| 219 |
+
border-radius: var(--radius-lg);
|
| 220 |
+
margin: 16px;
|
| 221 |
+
cursor: pointer;
|
| 222 |
+
transition: var(--transition);
|
| 223 |
+
position: relative;
|
| 224 |
+
}
|
| 225 |
+
.drop-zone:hover, .drop-zone.drag-over {
|
| 226 |
+
border-color: var(--purple);
|
| 227 |
+
background: var(--purple-glow);
|
| 228 |
+
}
|
| 229 |
+
.drop-icon {
|
| 230 |
+
width: 64px; height: 64px;
|
| 231 |
+
background: var(--purple-glow);
|
| 232 |
+
border: 1px solid rgba(124,58,237,0.25);
|
| 233 |
+
border-radius: var(--radius-lg);
|
| 234 |
+
display: flex; align-items: center; justify-content: center;
|
| 235 |
+
margin: 0 auto 20px; color: var(--purple-light);
|
| 236 |
+
transition: transform var(--transition);
|
| 237 |
+
}
|
| 238 |
+
.drop-zone:hover .drop-icon { transform: translateY(-4px); }
|
| 239 |
+
.drop-title { font-size: 1.15rem; font-weight: 700; margin-bottom: 6px; }
|
| 240 |
+
.drop-sub { color: var(--text-2); font-size: 0.875rem; margin-bottom: 24px; }
|
| 241 |
+
.drop-hint { color: var(--text-3); font-size: 0.78rem; margin-top: 14px; }
|
| 242 |
+
|
| 243 |
+
.preview-zone { padding: 28px; text-align: center; }
|
| 244 |
+
.preview-img-box {
|
| 245 |
+
border-radius: var(--radius-md);
|
| 246 |
+
overflow: hidden; border: 1px solid var(--border);
|
| 247 |
+
margin: 0 auto 16px; max-height: 280px;
|
| 248 |
+
background: rgba(0,0,0,0.3);
|
| 249 |
+
display: flex; align-items: center; justify-content: center;
|
| 250 |
+
}
|
| 251 |
+
.preview-img-box img { max-height: 280px; object-fit: contain; width: 100%; }
|
| 252 |
+
.preview-filename {
|
| 253 |
+
font-size: 0.85rem; font-weight: 600; color: var(--text-2);
|
| 254 |
+
margin-bottom: 20px; word-break: break-all;
|
| 255 |
+
}
|
| 256 |
+
.preview-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
| 257 |
+
|
| 258 |
+
/* ── Loading ─────────────────────────────────────────────────────── */
|
| 259 |
+
.loading-overlay {
|
| 260 |
+
position: fixed; inset: 0; z-index: 500;
|
| 261 |
+
background: rgba(8,8,16,0.88);
|
| 262 |
+
backdrop-filter: blur(12px);
|
| 263 |
+
display: flex; align-items: center; justify-content: center;
|
| 264 |
+
}
|
| 265 |
+
.loading-card {
|
| 266 |
+
background: var(--surface-solid);
|
| 267 |
+
border: 1px solid var(--border);
|
| 268 |
+
border-radius: var(--radius-xl);
|
| 269 |
+
padding: 44px 40px; width: 420px; max-width: 90vw;
|
| 270 |
+
box-shadow: var(--shadow-lg); text-align: center;
|
| 271 |
+
}
|
| 272 |
+
.spinner-ring {
|
| 273 |
+
width: 56px; height: 56px; margin: 0 auto 28px;
|
| 274 |
+
border: 3px solid var(--border);
|
| 275 |
+
border-top-color: var(--purple);
|
| 276 |
+
border-radius: 50%;
|
| 277 |
+
animation: spin 0.85s cubic-bezier(0.5, 0.1, 0.4, 0.9) infinite;
|
| 278 |
+
}
|
| 279 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 280 |
+
.loading-title { font-size: 1.15rem; font-weight: 700; margin-bottom: 6px; }
|
| 281 |
+
.loading-sub { font-size: 0.82rem; color: var(--text-2); margin-bottom: 28px; }
|
| 282 |
+
.steps-list { text-align: left; display: flex; flex-direction: column; gap: 6px; }
|
| 283 |
+
.step {
|
| 284 |
+
padding: 8px 14px; border-radius: var(--radius-sm);
|
| 285 |
+
font-size: 0.82rem; color: var(--text-3);
|
| 286 |
+
display: flex; align-items: center; gap: 10px;
|
| 287 |
+
transition: var(--transition);
|
| 288 |
+
}
|
| 289 |
+
.step::before { content: '○'; font-size: 0.7rem; flex-shrink: 0; }
|
| 290 |
+
.step.active { background: var(--purple-glow); color: var(--purple-light); font-weight: 600; }
|
| 291 |
+
.step.active::before { content: '●'; color: var(--purple); animation: pulse 1s infinite; }
|
| 292 |
+
.step.done { color: var(--green); }
|
| 293 |
+
.step.done::before { content: '✓'; color: var(--green); }
|
| 294 |
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
| 295 |
+
|
| 296 |
+
/* ── Result Section ─────────────────────────────────────────────── */
|
| 297 |
+
.result-section { padding: 16px 24px 64px; }
|
| 298 |
+
.result-wrap { max-width: 900px; margin: 0 auto; display: flex; flex-direction: column; gap: 28px; }
|
| 299 |
+
|
| 300 |
+
/* Verdict Card */
|
| 301 |
+
.verdict-card {
|
| 302 |
+
background: var(--surface);
|
| 303 |
+
border: 1px solid var(--border);
|
| 304 |
+
border-radius: var(--radius-xl);
|
| 305 |
+
backdrop-filter: blur(12px);
|
| 306 |
+
overflow: hidden;
|
| 307 |
+
box-shadow: var(--shadow-md);
|
| 308 |
+
}
|
| 309 |
+
.verdict-layout {
|
| 310 |
+
display: grid; grid-template-columns: 260px 1fr;
|
| 311 |
+
}
|
| 312 |
+
.result-img-panel {
|
| 313 |
+
background: rgba(0,0,0,0.25);
|
| 314 |
+
display: flex; align-items: center; justify-content: center;
|
| 315 |
+
padding: 28px;
|
| 316 |
+
border-right: 1px solid var(--border);
|
| 317 |
+
}
|
| 318 |
+
.result-img-panel img {
|
| 319 |
+
max-height: 220px; border-radius: var(--radius-md);
|
| 320 |
+
border: 1px solid var(--border);
|
| 321 |
+
object-fit: cover;
|
| 322 |
+
width: 100%;
|
| 323 |
+
}
|
| 324 |
+
.verdict-info { padding: 32px 36px; display: flex; flex-direction: column; gap: 20px; }
|
| 325 |
+
|
| 326 |
+
.verdict-badge {
|
| 327 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 328 |
+
padding: 6px 16px; border-radius: 999px;
|
| 329 |
+
font-size: 0.78rem; font-weight: 800;
|
| 330 |
+
text-transform: uppercase; letter-spacing: 0.05em;
|
| 331 |
+
width: fit-content;
|
| 332 |
+
}
|
| 333 |
+
.verdict-badge.is-ai {
|
| 334 |
+
background: var(--red-bg); color: var(--red);
|
| 335 |
+
border: 1px solid var(--red-border);
|
| 336 |
+
}
|
| 337 |
+
.verdict-badge.is-real {
|
| 338 |
+
background: var(--green-bg); color: var(--green);
|
| 339 |
+
border: 1px solid var(--green-border);
|
| 340 |
+
}
|
| 341 |
+
.verdict-badge svg { width: 12px; height: 12px; }
|
| 342 |
+
|
| 343 |
+
.verdict-heading { font-size: 1.8rem; font-weight: 800; letter-spacing: -0.025em; line-height: 1.2; }
|
| 344 |
+
.verdict-heading.is-ai { color: var(--red); }
|
| 345 |
+
.verdict-heading.is-real { color: var(--green); }
|
| 346 |
+
|
| 347 |
+
.confidence-label { font-size: 0.85rem; color: var(--text-2); font-weight: 500; }
|
| 348 |
+
|
| 349 |
+
.gauge-wrap {}
|
| 350 |
+
.gauge-meta {
|
| 351 |
+
display: flex; justify-content: space-between;
|
| 352 |
+
font-size: 0.75rem; color: var(--text-3); font-weight: 600; margin-bottom: 8px;
|
| 353 |
+
}
|
| 354 |
+
.gauge-track {
|
| 355 |
+
height: 10px; background: rgba(255,255,255,0.06);
|
| 356 |
+
border-radius: 999px; position: relative;
|
| 357 |
+
border: 1px solid var(--border);
|
| 358 |
+
}
|
| 359 |
+
.gauge-fill {
|
| 360 |
+
height: 100%; border-radius: 999px;
|
| 361 |
+
background: linear-gradient(90deg, var(--green) 0%, var(--orange) 48%, var(--red) 100%);
|
| 362 |
+
transition: width 1.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 363 |
+
}
|
| 364 |
+
.gauge-dot {
|
| 365 |
+
position: absolute; top: 50%;
|
| 366 |
+
transform: translate(-50%, -50%);
|
| 367 |
+
width: 18px; height: 18px; border-radius: 50%;
|
| 368 |
+
background: #fff; border: 3px solid var(--purple);
|
| 369 |
+
box-shadow: 0 0 0 3px var(--purple-glow), 0 2px 8px rgba(0,0,0,0.4);
|
| 370 |
+
transition: left 1.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 371 |
+
}
|
| 372 |
+
.gauge-vals {
|
| 373 |
+
display: flex; justify-content: space-between;
|
| 374 |
+
font-size: 0.82rem; font-weight: 700; margin-top: 8px;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.verdict-summary-text {
|
| 378 |
+
font-size: 0.9rem; line-height: 1.6; color: var(--text-2);
|
| 379 |
+
padding: 14px 18px;
|
| 380 |
+
background: rgba(255,255,255,0.03);
|
| 381 |
+
border-left: 3px solid var(--purple);
|
| 382 |
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
/* Why This Result */
|
| 386 |
+
.result-block-title {
|
| 387 |
+
font-size: 1.1rem; font-weight: 700; letter-spacing: -0.01em;
|
| 388 |
+
margin-bottom: 16px;
|
| 389 |
+
}
|
| 390 |
+
.why-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; }
|
| 391 |
+
.why-card {
|
| 392 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 393 |
+
border-radius: var(--radius-lg); padding: 22px;
|
| 394 |
+
transition: border-color var(--transition), transform var(--transition);
|
| 395 |
+
}
|
| 396 |
+
.why-card:hover { border-color: var(--border-hover); transform: translateY(-2px); }
|
| 397 |
+
.why-icon {
|
| 398 |
+
width: 38px; height: 38px; border-radius: var(--radius-sm);
|
| 399 |
+
display: flex; align-items: center; justify-content: center;
|
| 400 |
+
font-size: 1.1rem; margin-bottom: 14px;
|
| 401 |
+
}
|
| 402 |
+
.why-icon.purple { background: var(--purple-glow); border: 1px solid rgba(124,58,237,0.25); }
|
| 403 |
+
.why-icon.blue { background: rgba(59,130,246,0.12); border: 1px solid rgba(59,130,246,0.25); }
|
| 404 |
+
.why-icon.teal { background: rgba(20,184,166,0.12); border: 1px solid rgba(20,184,166,0.25); }
|
| 405 |
+
.why-card h4 { font-size: 0.9rem; font-weight: 700; margin-bottom: 8px; }
|
| 406 |
+
.why-card p { font-size: 0.82rem; color: var(--text-2); line-height: 1.55; }
|
| 407 |
+
|
| 408 |
+
/* AI Focus Areas */
|
| 409 |
+
.focus-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 20px; }
|
| 410 |
+
.focus-card {
|
| 411 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 412 |
+
border-radius: var(--radius-lg); padding: 22px;
|
| 413 |
+
}
|
| 414 |
+
.focus-card h4 { font-size: 0.9rem; font-weight: 700; margin-bottom: 4px; }
|
| 415 |
+
.focus-card p { font-size: 0.78rem; color: var(--text-3); margin-bottom: 14px; }
|
| 416 |
+
.heatmap-box {
|
| 417 |
+
border-radius: var(--radius-sm); overflow: hidden;
|
| 418 |
+
border: 1px solid var(--border);
|
| 419 |
+
background: #000; aspect-ratio: 4/3;
|
| 420 |
+
display: flex; align-items: center; justify-content: center;
|
| 421 |
+
}
|
| 422 |
+
.heatmap-box img { width: 100%; height: 100%; object-fit: contain; }
|
| 423 |
+
|
| 424 |
+
/* Advanced Toggle */
|
| 425 |
+
.adv-section { border-top: 1px solid var(--border); }
|
| 426 |
+
.adv-toggle {
|
| 427 |
+
width: 100%; background: none; border: none;
|
| 428 |
+
display: flex; align-items: center; justify-content: center; gap: 8px;
|
| 429 |
+
color: var(--text-3); font-size: 0.82rem; font-weight: 600;
|
| 430 |
+
padding: 18px 0; cursor: pointer;
|
| 431 |
+
transition: color var(--transition);
|
| 432 |
+
}
|
| 433 |
+
.adv-toggle:hover { color: var(--text-2); }
|
| 434 |
+
.adv-toggle svg { transition: transform 0.25s ease; }
|
| 435 |
+
.adv-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
|
| 436 |
+
.adv-body {
|
| 437 |
+
background: rgba(0,0,0,0.2); border-top: 1px solid var(--border);
|
| 438 |
+
padding: 24px;
|
| 439 |
+
}
|
| 440 |
+
.adv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap: 14px; }
|
| 441 |
+
.adv-card {
|
| 442 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 443 |
+
border-radius: var(--radius-md); padding: 16px;
|
| 444 |
+
}
|
| 445 |
+
.adv-card-title {
|
| 446 |
+
font-size: 0.8rem; font-weight: 700; color: var(--text-2);
|
| 447 |
+
margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.04em;
|
| 448 |
+
}
|
| 449 |
+
.adv-list { list-style: none; display: flex; flex-direction: column; gap: 7px; }
|
| 450 |
+
.adv-list li {
|
| 451 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 452 |
+
font-size: 0.78rem; color: var(--text-2);
|
| 453 |
+
padding-bottom: 6px; border-bottom: 1px solid var(--border);
|
| 454 |
+
}
|
| 455 |
+
.adv-list li:last-child { border: none; padding-bottom: 0; }
|
| 456 |
+
.adv-val { font-family: var(--mono); font-size: 0.75rem; color: var(--text); font-weight: 600; }
|
| 457 |
+
.adv-log {
|
| 458 |
+
grid-column: 1 / -1;
|
| 459 |
+
background: rgba(0,0,0,0.3); border: 1px solid var(--border);
|
| 460 |
+
border-radius: var(--radius-md); padding: 16px;
|
| 461 |
+
max-height: 180px; overflow-y: auto;
|
| 462 |
+
font-family: var(--mono); font-size: 0.73rem;
|
| 463 |
+
color: var(--text-2); line-height: 1.8;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.result-cta { text-align: center; padding-top: 8px; }
|
| 467 |
+
|
| 468 |
+
/* ── How It Works ────────────────────────────────────────────────── */
|
| 469 |
+
.how-section {
|
| 470 |
+
padding: 96px 24px;
|
| 471 |
+
background: var(--bg-secondary);
|
| 472 |
+
border-top: 1px solid var(--border);
|
| 473 |
+
}
|
| 474 |
+
.section-header { text-align: center; margin-bottom: 56px; }
|
| 475 |
+
.section-tag {
|
| 476 |
+
display: inline-block;
|
| 477 |
+
background: var(--purple-glow); color: var(--purple-light);
|
| 478 |
+
border: 1px solid rgba(124,58,237,0.25);
|
| 479 |
+
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
| 480 |
+
letter-spacing: 0.06em; padding: 4px 14px; border-radius: 999px; margin-bottom: 14px;
|
| 481 |
+
}
|
| 482 |
+
.section-title { font-size: clamp(1.6rem, 3.5vw, 2.2rem); font-weight: 800; letter-spacing: -0.025em; margin-bottom: 10px; }
|
| 483 |
+
.section-desc { font-size: 0.95rem; color: var(--text-2); max-width: 520px; margin: 0 auto; }
|
| 484 |
+
|
| 485 |
+
.how-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 24px; }
|
| 486 |
+
.how-card {
|
| 487 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 488 |
+
border-radius: var(--radius-lg); padding: 32px 24px;
|
| 489 |
+
text-align: center; transition: var(--transition);
|
| 490 |
+
position: relative; overflow: hidden;
|
| 491 |
+
}
|
| 492 |
+
.how-card::before {
|
| 493 |
+
content: '';
|
| 494 |
+
position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
| 495 |
+
background: var(--purple-grad); opacity: 0;
|
| 496 |
+
transition: opacity var(--transition);
|
| 497 |
+
}
|
| 498 |
+
.how-card:hover { border-color: var(--border-hover); transform: translateY(-4px); box-shadow: var(--shadow-md); }
|
| 499 |
+
.how-card:hover::before { opacity: 1; }
|
| 500 |
+
.how-num {
|
| 501 |
+
font-size: 2.4rem; font-weight: 800; color: var(--purple-light);
|
| 502 |
+
opacity: 0.35; line-height: 1; margin-bottom: 20px;
|
| 503 |
+
}
|
| 504 |
+
.how-card h3 { font-size: 1rem; font-weight: 700; margin-bottom: 10px; }
|
| 505 |
+
.how-card p { font-size: 0.85rem; color: var(--text-2); line-height: 1.55; }
|
| 506 |
+
|
| 507 |
+
/* ── About Section ───────────────────────────────────────────────── */
|
| 508 |
+
.about-section { padding: 96px 24px; border-top: 1px solid var(--border); }
|
| 509 |
+
.about-grid { display: grid; grid-template-columns: 1fr 420px; gap: 60px; align-items: center; }
|
| 510 |
+
.about-text .section-tag { margin-bottom: 14px; }
|
| 511 |
+
.about-text h2 { font-size: clamp(1.6rem, 3vw, 2.2rem); font-weight: 800; letter-spacing: -0.025em; margin-bottom: 20px; }
|
| 512 |
+
.about-text p { font-size: 0.92rem; color: var(--text-2); line-height: 1.75; margin-bottom: 14px; }
|
| 513 |
+
.about-visual {
|
| 514 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 515 |
+
border-radius: var(--radius-xl); padding: 36px;
|
| 516 |
+
position: relative; overflow: hidden;
|
| 517 |
+
}
|
| 518 |
+
.about-visual::before {
|
| 519 |
+
content: '';
|
| 520 |
+
position: absolute; top: -60px; right: -60px; width: 200px; height: 200px;
|
| 521 |
+
background: radial-gradient(circle, rgba(124,58,237,0.25) 0%, transparent 70%);
|
| 522 |
+
pointer-events: none;
|
| 523 |
+
}
|
| 524 |
+
.about-stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
| 525 |
+
.about-stat {
|
| 526 |
+
background: rgba(0,0,0,0.2); border: 1px solid var(--border);
|
| 527 |
+
border-radius: var(--radius-md); padding: 20px;
|
| 528 |
+
}
|
| 529 |
+
.about-stat-num { font-size: 1.6rem; font-weight: 800; color: var(--purple-light); margin-bottom: 4px; }
|
| 530 |
+
.about-stat-label { font-size: 0.78rem; color: var(--text-2); font-weight: 500; }
|
| 531 |
+
|
| 532 |
+
/* ── Project Highlights ──────────────────────────────────────────── */
|
| 533 |
+
.highlights-section {
|
| 534 |
+
padding: 96px 24px;
|
| 535 |
+
background: var(--bg-secondary);
|
| 536 |
+
border-top: 1px solid var(--border);
|
| 537 |
+
}
|
| 538 |
+
.highlights-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 20px; }
|
| 539 |
+
.highlight-card {
|
| 540 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 541 |
+
border-radius: var(--radius-lg); padding: 28px;
|
| 542 |
+
transition: var(--transition);
|
| 543 |
+
}
|
| 544 |
+
.highlight-card:hover { border-color: rgba(124,58,237,0.35); box-shadow: 0 0 24px rgba(124,58,237,0.10); transform: translateY(-2px); }
|
| 545 |
+
.highlight-icon {
|
| 546 |
+
width: 46px; height: 46px; border-radius: var(--radius-md);
|
| 547 |
+
background: var(--purple-glow); border: 1px solid rgba(124,58,237,0.25);
|
| 548 |
+
display: flex; align-items: center; justify-content: center;
|
| 549 |
+
font-size: 1.3rem; margin-bottom: 16px;
|
| 550 |
+
}
|
| 551 |
+
.highlight-card h4 { font-size: 0.95rem; font-weight: 700; margin-bottom: 8px; }
|
| 552 |
+
.highlight-card p { font-size: 0.82rem; color: var(--text-2); line-height: 1.5; }
|
| 553 |
+
|
| 554 |
+
/* ── Tech Stack ──────────────────────────────────────────────────── */
|
| 555 |
+
.tech-section { padding: 96px 24px; border-top: 1px solid var(--border); }
|
| 556 |
+
.tech-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 20px; }
|
| 557 |
+
.tech-group {
|
| 558 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 559 |
+
border-radius: var(--radius-lg); padding: 24px;
|
| 560 |
+
}
|
| 561 |
+
.tech-group-label {
|
| 562 |
+
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
| 563 |
+
letter-spacing: 0.06em; color: var(--text-3); margin-bottom: 16px;
|
| 564 |
+
}
|
| 565 |
+
.tech-chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
| 566 |
+
.tech-chip {
|
| 567 |
+
display: flex; align-items: center; gap: 6px;
|
| 568 |
+
background: rgba(255,255,255,0.05); border: 1px solid var(--border);
|
| 569 |
+
border-radius: var(--radius-xs); padding: 5px 11px;
|
| 570 |
+
font-size: 0.78rem; font-weight: 600; color: var(--text-2);
|
| 571 |
+
transition: var(--transition);
|
| 572 |
+
}
|
| 573 |
+
.tech-chip:hover { border-color: var(--purple); color: var(--purple-light); background: var(--purple-glow); }
|
| 574 |
+
.tech-chip-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
| 575 |
+
|
| 576 |
+
/* ── Team Section ────────────────────────────────────────────────── */
|
| 577 |
+
.team-section {
|
| 578 |
+
padding: 96px 24px;
|
| 579 |
+
background: var(--bg-secondary);
|
| 580 |
+
border-top: 1px solid var(--border);
|
| 581 |
+
}
|
| 582 |
+
.team-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 20px; }
|
| 583 |
+
.team-card {
|
| 584 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 585 |
+
border-radius: var(--radius-lg); padding: 28px 24px;
|
| 586 |
+
text-align: center; transition: var(--transition);
|
| 587 |
+
position: relative; overflow: hidden;
|
| 588 |
+
}
|
| 589 |
+
.team-card::after {
|
| 590 |
+
content: '';
|
| 591 |
+
position: absolute; inset: 0;
|
| 592 |
+
background: linear-gradient(135deg, rgba(124,58,237,0.06) 0%, transparent 60%);
|
| 593 |
+
opacity: 0; transition: opacity var(--transition);
|
| 594 |
+
pointer-events: none;
|
| 595 |
+
}
|
| 596 |
+
.team-card:hover { border-color: rgba(124,58,237,0.35); transform: translateY(-4px); box-shadow: var(--shadow-md); }
|
| 597 |
+
.team-card:hover::after { opacity: 1; }
|
| 598 |
+
.team-avatar {
|
| 599 |
+
width: 72px; height: 72px; border-radius: 50%;
|
| 600 |
+
background: var(--purple-grad); display: flex; align-items: center;
|
| 601 |
+
justify-content: center; font-size: 1.5rem; font-weight: 800;
|
| 602 |
+
color: #fff; margin: 0 auto 18px;
|
| 603 |
+
box-shadow: 0 0 0 4px rgba(124,58,237,0.20);
|
| 604 |
+
}
|
| 605 |
+
.team-avatar--photo { background-size: cover; background-position: center top; background-color: transparent; font-size: 0; }
|
| 606 |
+
.team-avatar--photo img { display: none; }
|
| 607 |
+
.team-name { font-size: 1rem; font-weight: 700; margin-bottom: 4px; }
|
| 608 |
+
.team-role {
|
| 609 |
+
font-size: 0.78rem; font-weight: 600;
|
| 610 |
+
color: var(--purple-light); margin-bottom: 8px;
|
| 611 |
+
}
|
| 612 |
+
.team-branch {
|
| 613 |
+
display: inline-block; font-size: 0.7rem; font-weight: 700;
|
| 614 |
+
background: var(--purple-glow); color: var(--purple-light);
|
| 615 |
+
border: 1px solid rgba(124,58,237,0.20);
|
| 616 |
+
padding: 2px 10px; border-radius: 999px; margin-bottom: 14px;
|
| 617 |
+
}
|
| 618 |
+
.team-desc { font-size: 0.79rem; color: var(--text-2); line-height: 1.55; margin-bottom: 20px; }
|
| 619 |
+
.team-links { display: flex; justify-content: center; gap: 10px; }
|
| 620 |
+
.team-link {
|
| 621 |
+
width: 34px; height: 34px; border-radius: var(--radius-sm);
|
| 622 |
+
background: var(--surface-hover); border: 1px solid var(--border);
|
| 623 |
+
display: flex; align-items: center; justify-content: center;
|
| 624 |
+
color: var(--text-2); transition: var(--transition); font-size: 0.85rem;
|
| 625 |
+
}
|
| 626 |
+
.team-link:hover { border-color: var(--purple); color: var(--purple-light); background: var(--purple-glow); }
|
| 627 |
+
|
| 628 |
+
/* ── Footer ──────────────────────────────────────────────────────── */
|
| 629 |
+
.footer { padding: 60px 24px 40px; border-top: 1px solid var(--border); }
|
| 630 |
+
.footer-inner { max-width: 1100px; margin: 0 auto; }
|
| 631 |
+
.footer-top { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 48px; margin-bottom: 48px; }
|
| 632 |
+
.footer-brand-name { font-size: 1.1rem; font-weight: 800; margin-bottom: 10px; letter-spacing: -0.01em; }
|
| 633 |
+
.footer-brand-desc { font-size: 0.83rem; color: var(--text-2); line-height: 1.6; max-width: 280px; }
|
| 634 |
+
.footer-privacy-note {
|
| 635 |
+
margin-top: 16px;
|
| 636 |
+
font-size: 0.75rem; color: var(--text-3);
|
| 637 |
+
display: flex; align-items: center; gap: 6px;
|
| 638 |
+
}
|
| 639 |
+
.footer-col h5 { font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-3); margin-bottom: 16px; }
|
| 640 |
+
.footer-col ul { list-style: none; display: flex; flex-direction: column; gap: 10px; }
|
| 641 |
+
.footer-col li { font-size: 0.83rem; color: var(--text-2); }
|
| 642 |
+
.footer-col a { transition: color var(--transition); }
|
| 643 |
+
.footer-col a:hover { color: var(--purple-light); }
|
| 644 |
+
.footer-bottom {
|
| 645 |
+
border-top: 1px solid var(--border); padding-top: 28px;
|
| 646 |
+
display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 16px;
|
| 647 |
+
}
|
| 648 |
+
.footer-copy { font-size: 0.78rem; color: var(--text-3); }
|
| 649 |
+
.footer-attribution { font-size: 0.72rem; color: var(--text-3); line-height: 1.6; }
|
| 650 |
+
.footer-attribution a { color: var(--purple-light); }
|
| 651 |
+
.footer-attribution a:hover { text-decoration: underline; }
|
| 652 |
+
|
| 653 |
+
/* ── Divider ─────────────────────────────────────────────────────── */
|
| 654 |
+
.divider {
|
| 655 |
+
height: 1px; background: linear-gradient(90deg, transparent, var(--border), transparent);
|
| 656 |
+
border: none; margin: 0;
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
/* ── Responsive ──────────────────────────────────────────────────── */
|
| 660 |
+
@media (max-width: 1024px) {
|
| 661 |
+
.tech-grid { grid-template-columns: repeat(2,1fr); }
|
| 662 |
+
.team-grid { grid-template-columns: repeat(2,1fr); }
|
| 663 |
+
.about-grid { grid-template-columns: 1fr; }
|
| 664 |
+
.about-visual { max-width: 500px; }
|
| 665 |
+
.footer-top { grid-template-columns: 1fr 1fr; gap: 32px; }
|
| 666 |
+
}
|
| 667 |
+
@media (max-width: 768px) {
|
| 668 |
+
.hero { padding: 72px 20px 48px; }
|
| 669 |
+
.hero-stats { gap: 28px; }
|
| 670 |
+
.verdict-layout { grid-template-columns: 1fr; }
|
| 671 |
+
.result-img-panel { border-right: none; border-bottom: 1px solid var(--border); padding: 24px; }
|
| 672 |
+
.result-img-panel img { max-height: 200px; }
|
| 673 |
+
.verdict-info { padding: 24px; gap: 16px; }
|
| 674 |
+
.verdict-heading { font-size: 1.5rem; }
|
| 675 |
+
.why-grid { grid-template-columns: 1fr; gap: 12px; }
|
| 676 |
+
.focus-grid { grid-template-columns: 1fr; }
|
| 677 |
+
.how-grid { grid-template-columns: 1fr; gap: 16px; }
|
| 678 |
+
.highlights-grid { grid-template-columns: 1fr; gap: 14px; }
|
| 679 |
+
.tech-grid { grid-template-columns: 1fr 1fr; }
|
| 680 |
+
.team-grid { grid-template-columns: 1fr 1fr; }
|
| 681 |
+
.nav-link { display: none; }
|
| 682 |
+
.footer-top { grid-template-columns: 1fr; gap: 28px; }
|
| 683 |
+
.footer-bottom { flex-direction: column; text-align: center; }
|
| 684 |
+
}
|
| 685 |
+
@media (max-width: 480px) {
|
| 686 |
+
.hero h1 { font-size: 1.9rem; }
|
| 687 |
+
.team-grid { grid-template-columns: 1fr; }
|
| 688 |
+
.tech-grid { grid-template-columns: 1fr; }
|
| 689 |
+
.preview-actions { flex-direction: column; }
|
| 690 |
+
.preview-actions .btn { width: 100%; }
|
| 691 |
+
.upload-section { padding: 0 16px 64px; }
|
| 692 |
+
.result-section { padding: 16px 16px 48px; }
|
| 693 |
+
}
|
frontend/static/js/app.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ═══════════════════════════════════════════════════════════════
|
| 2 |
+
ImgAuth AI — Frontend Logic
|
| 3 |
+
─ Binary verdict: Likely AI-Generated vs Likely Authentic
|
| 4 |
+
─ Bug fix: result section scrolls into view after analysis
|
| 5 |
+
═══════════════════════════════════════════════════════════════ */
|
| 6 |
+
|
| 7 |
+
// ── Theme Toggle ──────────────────────────────────────────────────
|
| 8 |
+
(function () {
|
| 9 |
+
const html = document.documentElement;
|
| 10 |
+
const btn = document.querySelector('[data-theme-toggle]');
|
| 11 |
+
let theme = localStorage.getItem('imgauth-theme') || 'dark';
|
| 12 |
+
|
| 13 |
+
html.setAttribute('data-theme', theme);
|
| 14 |
+
if (btn) btn.textContent = theme === 'dark' ? '🌙' : '☀️';
|
| 15 |
+
|
| 16 |
+
if (btn) {
|
| 17 |
+
btn.addEventListener('click', () => {
|
| 18 |
+
theme = theme === 'dark' ? 'light' : 'dark';
|
| 19 |
+
html.setAttribute('data-theme', theme);
|
| 20 |
+
btn.textContent = theme === 'dark' ? '🌙' : '☀️';
|
| 21 |
+
localStorage.setItem('imgauth-theme', theme);
|
| 22 |
+
});
|
| 23 |
+
}
|
| 24 |
+
})();
|
| 25 |
+
|
| 26 |
+
// ── Element Refs ──────────────────────────────────────────────────
|
| 27 |
+
const fileInput = document.getElementById('fileInput');
|
| 28 |
+
const dropZone = document.getElementById('dropZone');
|
| 29 |
+
const previewZone = document.getElementById('previewZone');
|
| 30 |
+
const previewImg = document.getElementById('previewImg');
|
| 31 |
+
const previewFilename = document.getElementById('previewFilename');
|
| 32 |
+
const analyzeBtn = document.getElementById('analyzeBtn');
|
| 33 |
+
const clearBtn = document.getElementById('clearBtn');
|
| 34 |
+
const loadingOverlay = document.getElementById('loadingOverlay');
|
| 35 |
+
const resultSection = document.getElementById('resultSection');
|
| 36 |
+
const newScanBtn = document.getElementById('newScanBtn');
|
| 37 |
+
|
| 38 |
+
let selectedFile = null;
|
| 39 |
+
|
| 40 |
+
// ── File Handling ─────────────────────────────────────────────────
|
| 41 |
+
fileInput.addEventListener('change', e => handleFile(e.target.files[0]));
|
| 42 |
+
|
| 43 |
+
dropZone.addEventListener('dragover', e => {
|
| 44 |
+
e.preventDefault();
|
| 45 |
+
dropZone.classList.add('drag-over');
|
| 46 |
+
});
|
| 47 |
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
| 48 |
+
dropZone.addEventListener('drop', e => {
|
| 49 |
+
e.preventDefault();
|
| 50 |
+
dropZone.classList.remove('drag-over');
|
| 51 |
+
const f = e.dataTransfer.files[0];
|
| 52 |
+
if (f) handleFile(f);
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
function handleFile(file) {
|
| 56 |
+
if (!file || !file.type.startsWith('image/')) {
|
| 57 |
+
showToast('Please select a valid image file (PNG, JPG, WEBP).', 'error');
|
| 58 |
+
return;
|
| 59 |
+
}
|
| 60 |
+
if (file.size > 10 * 1024 * 1024) {
|
| 61 |
+
showToast('File too large. Maximum size is 10 MB.', 'error');
|
| 62 |
+
return;
|
| 63 |
+
}
|
| 64 |
+
selectedFile = file;
|
| 65 |
+
const reader = new FileReader();
|
| 66 |
+
reader.onload = ev => {
|
| 67 |
+
previewImg.src = ev.target.result;
|
| 68 |
+
previewFilename.textContent = file.name;
|
| 69 |
+
dropZone.style.display = 'none';
|
| 70 |
+
previewZone.style.display = 'block';
|
| 71 |
+
resultSection.style.display = 'none';
|
| 72 |
+
};
|
| 73 |
+
reader.readAsDataURL(file);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
clearBtn.addEventListener('click', resetToUpload);
|
| 77 |
+
|
| 78 |
+
// ── Analyze ───────────────────────────────────────────────────────
|
| 79 |
+
analyzeBtn.addEventListener('click', async () => {
|
| 80 |
+
if (!selectedFile) return;
|
| 81 |
+
showLoading();
|
| 82 |
+
try {
|
| 83 |
+
const form = new FormData();
|
| 84 |
+
form.append('file', selectedFile);
|
| 85 |
+
const res = await fetch('/api/detect', { method: 'POST', body: form });
|
| 86 |
+
const data = await res.json();
|
| 87 |
+
if (!res.ok) throw new Error(data.detail || 'Server error');
|
| 88 |
+
hideLoading();
|
| 89 |
+
showResult(data);
|
| 90 |
+
} catch (err) {
|
| 91 |
+
hideLoading();
|
| 92 |
+
showToast('Error: ' + err.message, 'error');
|
| 93 |
+
}
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
newScanBtn.addEventListener('click', resetToUpload);
|
| 97 |
+
|
| 98 |
+
function resetToUpload() {
|
| 99 |
+
resultSection.style.display = 'none';
|
| 100 |
+
previewZone.style.display = 'none';
|
| 101 |
+
dropZone.style.display = 'block';
|
| 102 |
+
selectedFile = null;
|
| 103 |
+
fileInput.value = '';
|
| 104 |
+
// Scroll back up to the upload section
|
| 105 |
+
document.getElementById('uploadSection').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// ── Loading Steps ─────────────────────────────────────────────────
|
| 109 |
+
let stepTimer = null;
|
| 110 |
+
const STEP_IDS = ['step1','step2','step3','step4','step5','step6'];
|
| 111 |
+
|
| 112 |
+
function showLoading() {
|
| 113 |
+
// Reset all steps
|
| 114 |
+
STEP_IDS.forEach(id => {
|
| 115 |
+
const el = document.getElementById(id);
|
| 116 |
+
if (el) el.className = 'step';
|
| 117 |
+
});
|
| 118 |
+
loadingOverlay.style.display = 'flex';
|
| 119 |
+
let i = 0;
|
| 120 |
+
stepTimer = setInterval(() => {
|
| 121 |
+
if (i < STEP_IDS.length) {
|
| 122 |
+
if (i > 0) {
|
| 123 |
+
const prev = document.getElementById(STEP_IDS[i - 1]);
|
| 124 |
+
if (prev) prev.className = 'step done';
|
| 125 |
+
}
|
| 126 |
+
const cur = document.getElementById(STEP_IDS[i]);
|
| 127 |
+
if (cur) cur.className = 'step active';
|
| 128 |
+
i++;
|
| 129 |
+
}
|
| 130 |
+
}, 700);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function hideLoading() {
|
| 134 |
+
clearInterval(stepTimer);
|
| 135 |
+
loadingOverlay.style.display = 'none';
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// ── Show Result ───────────────────────────────────────────────────
|
| 139 |
+
function showResult(data) {
|
| 140 |
+
const aiScore = typeof data.ai_score === 'number' ? data.ai_score : 50;
|
| 141 |
+
const realScore = typeof data.real_score === 'number' ? data.real_score : 50;
|
| 142 |
+
|
| 143 |
+
// ── Populate result image
|
| 144 |
+
document.getElementById('resultImg').src = previewImg.src;
|
| 145 |
+
|
| 146 |
+
// ── Binary verdict (no "Uncertain" category)
|
| 147 |
+
const isAI = aiScore > 50;
|
| 148 |
+
|
| 149 |
+
const badge = document.getElementById('resultBadge');
|
| 150 |
+
const verdict = document.getElementById('resultVerdict');
|
| 151 |
+
|
| 152 |
+
if (isAI) {
|
| 153 |
+
badge.textContent = '🔴 AI-Generated';
|
| 154 |
+
badge.className = 'verdict-badge is-ai';
|
| 155 |
+
verdict.textContent = 'Likely AI-Generated';
|
| 156 |
+
verdict.className = 'verdict-heading is-ai';
|
| 157 |
+
} else {
|
| 158 |
+
badge.textContent = '🟢 Authentic';
|
| 159 |
+
badge.className = 'verdict-badge is-real';
|
| 160 |
+
verdict.textContent = 'Likely Authentic';
|
| 161 |
+
verdict.className = 'verdict-heading is-real';
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// ── Confidence label — based on distance from 50%
|
| 165 |
+
const margin = Math.abs(aiScore - 50);
|
| 166 |
+
let confidenceLabel;
|
| 167 |
+
if (margin >= 35) confidenceLabel = 'Very High Confidence';
|
| 168 |
+
else if (margin >= 20) confidenceLabel = 'High Confidence';
|
| 169 |
+
else if (margin >= 10) confidenceLabel = 'Medium Confidence';
|
| 170 |
+
else confidenceLabel = 'Low Confidence';
|
| 171 |
+
document.getElementById('resultConfidenceLevel').textContent = confidenceLabel;
|
| 172 |
+
|
| 173 |
+
// ── Gauge bar (animate after a tick so CSS transition fires)
|
| 174 |
+
setTimeout(() => {
|
| 175 |
+
document.getElementById('gaugeFill').style.width = aiScore + '%';
|
| 176 |
+
document.getElementById('gaugeDot').style.left = aiScore + '%';
|
| 177 |
+
document.getElementById('realPct').textContent = realScore.toFixed(1) + '%';
|
| 178 |
+
document.getElementById('aiPct').textContent = aiScore.toFixed(1) + '%';
|
| 179 |
+
}, 80);
|
| 180 |
+
|
| 181 |
+
// ── Plain-language summary
|
| 182 |
+
document.getElementById('resultSummary').textContent = buildSummary(aiScore);
|
| 183 |
+
|
| 184 |
+
// ── "Why this result" cards
|
| 185 |
+
buildWhyCards(data, aiScore);
|
| 186 |
+
|
| 187 |
+
// ── Heatmaps
|
| 188 |
+
buildHeatmaps(data);
|
| 189 |
+
|
| 190 |
+
// ── Advanced technical section
|
| 191 |
+
buildAdvanced(data);
|
| 192 |
+
|
| 193 |
+
// ── Advanced toggle — clone to remove stale listeners
|
| 194 |
+
const oldBtn = document.getElementById('advancedToggle');
|
| 195 |
+
const newBtn = oldBtn.cloneNode(true);
|
| 196 |
+
oldBtn.parentNode.replaceChild(newBtn, oldBtn);
|
| 197 |
+
newBtn.addEventListener('click', function () {
|
| 198 |
+
const body = document.getElementById('advancedBody');
|
| 199 |
+
const isOpen = body.style.display === 'block';
|
| 200 |
+
body.style.display = isOpen ? 'none' : 'block';
|
| 201 |
+
this.setAttribute('aria-expanded', String(!isOpen));
|
| 202 |
+
this.querySelector('span').textContent = isOpen
|
| 203 |
+
? 'Show Technical Analysis'
|
| 204 |
+
: 'Hide Technical Analysis';
|
| 205 |
+
});
|
| 206 |
+
|
| 207 |
+
// ── Show result section then scroll TO IT ← BUG FIX
|
| 208 |
+
resultSection.style.display = 'block';
|
| 209 |
+
setTimeout(() => {
|
| 210 |
+
resultSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 211 |
+
}, 60);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// ── Build plain-language summary ──────────────────────────────────
|
| 215 |
+
function buildSummary(aiScore) {
|
| 216 |
+
if (aiScore >= 80) return 'This image shows strong patterns typically found in AI-generated content. Multiple models flagged it with high confidence.';
|
| 217 |
+
if (aiScore >= 60) return 'This image shows several patterns commonly associated with AI-generated images.';
|
| 218 |
+
if (aiScore >= 50) return 'This image leans towards AI-generated, but the signal is relatively weak.';
|
| 219 |
+
if (aiScore >= 35) return 'This image appears to be authentic, though some minor signals were detected.';
|
| 220 |
+
return 'This image shows patterns consistent with authentic, real-world photographs.';
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// ── Build "Why this result" cards ─────────────────────────────────
|
| 224 |
+
function buildWhyCards(data, aiScore) {
|
| 225 |
+
// Visual Patterns
|
| 226 |
+
let visualText;
|
| 227 |
+
if (aiScore >= 70)
|
| 228 |
+
visualText = 'Detected unusual texture patterns and edge artifacts commonly found in AI-generated images.';
|
| 229 |
+
else if (aiScore >= 50)
|
| 230 |
+
visualText = 'Some visual patterns are consistent with AI generation, though results are mixed.';
|
| 231 |
+
else
|
| 232 |
+
visualText = 'Visual patterns appear natural and consistent with authentic photographs.';
|
| 233 |
+
document.getElementById('explainVisual').textContent = visualText;
|
| 234 |
+
|
| 235 |
+
// AI Model Analysis
|
| 236 |
+
let modelText;
|
| 237 |
+
if (aiScore >= 70)
|
| 238 |
+
modelText = 'Multiple AI detection models identified patterns strongly associated with AI-generated images.';
|
| 239 |
+
else if (aiScore >= 50)
|
| 240 |
+
modelText = 'AI detection models show a mild lean toward generated content.';
|
| 241 |
+
else
|
| 242 |
+
modelText = 'AI models detected characteristics more consistent with real, authentic photographs.';
|
| 243 |
+
document.getElementById('explainModels').textContent = modelText;
|
| 244 |
+
|
| 245 |
+
// Metadata Check
|
| 246 |
+
let metaText = 'No reliable camera metadata was found. This may indicate the image was generated or heavily edited.';
|
| 247 |
+
const mdLayer = (data.breakdown || []).find(b => b.layer === 'Metadata Analysis');
|
| 248 |
+
if (mdLayer && mdLayer.signals) {
|
| 249 |
+
const sigs = mdLayer.signals.join(' ');
|
| 250 |
+
if (sigs.includes('camera EXIF') || sigs.includes('GPS'))
|
| 251 |
+
metaText = 'Camera metadata was found, suggesting the image may have been captured with a real device.';
|
| 252 |
+
else if (sigs.includes('Software = AI') || sigs.includes('PNG metadata key'))
|
| 253 |
+
metaText = 'Metadata explicitly references AI generation tools — a strong indicator of synthetic origin.';
|
| 254 |
+
}
|
| 255 |
+
document.getElementById('explainMetadata').textContent = metaText;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// ── Build Heatmaps ────────────────────────────────────────────────
|
| 259 |
+
function buildHeatmaps(data) {
|
| 260 |
+
const focusAreas = document.getElementById('focusAreas');
|
| 261 |
+
const dfiCard = document.getElementById('dfiCardContainer');
|
| 262 |
+
const attImg = document.getElementById('attentionHeatmapImg');
|
| 263 |
+
const dfiImg = document.getElementById('dfiHeatmapImg');
|
| 264 |
+
|
| 265 |
+
let hasAny = false;
|
| 266 |
+
const ml = data.layers && data.layers.models ? data.layers.models : {};
|
| 267 |
+
|
| 268 |
+
if (ml.attention_heatmap) {
|
| 269 |
+
attImg.src = ml.attention_heatmap;
|
| 270 |
+
hasAny = true;
|
| 271 |
+
}
|
| 272 |
+
if (ml.dfi_heatmap) {
|
| 273 |
+
dfiImg.src = ml.dfi_heatmap;
|
| 274 |
+
dfiCard.style.display = 'block';
|
| 275 |
+
hasAny = true;
|
| 276 |
+
} else {
|
| 277 |
+
dfiCard.style.display = 'none';
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
focusAreas.style.display = hasAny ? 'block' : 'none';
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
// ── Build Advanced Technical Section ─────────────────────────────
|
| 284 |
+
function buildAdvanced(data) {
|
| 285 |
+
const container = document.getElementById('advancedContent');
|
| 286 |
+
container.innerHTML = '';
|
| 287 |
+
|
| 288 |
+
const fnLayer = (data.breakdown || []).find(b => b.layer === 'Filename Analysis');
|
| 289 |
+
const mdLayer = (data.breakdown || []).find(b => b.layer === 'Metadata Analysis');
|
| 290 |
+
const mlLayer = (data.breakdown || []).find(b => b.layer === 'AI Model and Forensic Detectors');
|
| 291 |
+
|
| 292 |
+
const votes = mlLayer ? (mlLayer.votes || []) : [];
|
| 293 |
+
const forensics = mlLayer ? (mlLayer.forensics || {}) : {};
|
| 294 |
+
|
| 295 |
+
// Card 1 — Model Results
|
| 296 |
+
const dlVotes = votes.filter(v => v.type === 'deep_learning');
|
| 297 |
+
const modelItems = dlVotes.length
|
| 298 |
+
? dlVotes.map(v => `<li><span>${esc(v.detector)}</span><span class="adv-val">${(v.ai_prob * 100).toFixed(1)}% AI</span></li>`).join('')
|
| 299 |
+
: '<li><span>No models available</span></li>';
|
| 300 |
+
container.appendChild(makeAdvCard('Model Results', modelItems));
|
| 301 |
+
|
| 302 |
+
// Card 2 — Forensic Signatures
|
| 303 |
+
const fmtNum = (v, dec = 4) => (v !== undefined && v !== null) ? Number(v).toFixed(dec) : 'N/A';
|
| 304 |
+
const kurtosis = forensics.kurtosis || {};
|
| 305 |
+
const dfi = forensics.dfi || {};
|
| 306 |
+
const fft = forensics.fft || {};
|
| 307 |
+
const chist = forensics.color_histogram || {};
|
| 308 |
+
const jpeg = forensics.jpeg_ghost || {};
|
| 309 |
+
const jpegNA = jpeg.detail === 'Not a JPEG' || !jpeg.ghost_spread;
|
| 310 |
+
const forensicItems = `
|
| 311 |
+
<li><span>Noise Kurtosis</span> <span class="adv-val">${fmtNum(kurtosis.kurtosis)}</span></li>
|
| 312 |
+
<li><span>DFI Variance</span> <span class="adv-val">${fmtNum(dfi.variance, 5)}</span></li>
|
| 313 |
+
<li><span>FFT Spike Ratio</span> <span class="adv-val">${fmtNum(fft.spike_ratio, 5)}</span></li>
|
| 314 |
+
<li><span>Histogram Roughness</span><span class="adv-val">${fmtNum(chist.roughness, 6)}</span></li>
|
| 315 |
+
<li><span>JPEG Ghost Spread</span> <span class="adv-val">${jpegNA ? 'N/A (non-JPEG)' : fmtNum(jpeg.ghost_spread, 3)}</span></li>
|
| 316 |
+
`;
|
| 317 |
+
container.appendChild(makeAdvCard('Advanced Signals', forensicItems));
|
| 318 |
+
|
| 319 |
+
// Card 3 — Layer Weights
|
| 320 |
+
const weightItems = `
|
| 321 |
+
<li><span>Filename Weight</span> <span class="adv-val">${fnLayer ? fnLayer.weight_pct : '0%'}</span></li>
|
| 322 |
+
<li><span>Filename AI Score</span><span class="adv-val">${fnLayer ? fnLayer.ai_pts + ' pts' : '—'}</span></li>
|
| 323 |
+
<li><span>Metadata Weight</span> <span class="adv-val">${mdLayer ? mdLayer.weight_pct : '0%'}</span></li>
|
| 324 |
+
<li><span>Metadata AI Score</span><span class="adv-val">${mdLayer ? mdLayer.ai_pts + ' pts' : '—'}</span></li>
|
| 325 |
+
<li><span>Model & Forensic Wt</span><span class="adv-val">${mlLayer ? mlLayer.weight_pct : '0%'}</span></li>
|
| 326 |
+
<li><span>Model AI Score</span> <span class="adv-val">${mlLayer ? mlLayer.ai_pts + ' pts' : '—'}</span></li>
|
| 327 |
+
`;
|
| 328 |
+
container.appendChild(makeAdvCard('Layer Breakdown', weightItems));
|
| 329 |
+
|
| 330 |
+
// Signal Log — full width
|
| 331 |
+
const allSignals = [
|
| 332 |
+
...(fnLayer ? fnLayer.signals || [] : []),
|
| 333 |
+
...(mdLayer ? mdLayer.signals || [] : []),
|
| 334 |
+
...(mlLayer ? mlLayer.signals || [] : []),
|
| 335 |
+
];
|
| 336 |
+
const logHtml = allSignals.length
|
| 337 |
+
? allSignals.map(s => {
|
| 338 |
+
const clean = esc(s)
|
| 339 |
+
.replace(/^\[AI\]\s*/i, '<span style="color:var(--red)">⚠ </span>')
|
| 340 |
+
.replace(/^\[REAL\]\s*/i, '<span style="color:var(--green)">✓ </span>')
|
| 341 |
+
.replace(/^\[INFO\]\s*/i, '<span style="color:var(--text-3)">ℹ </span>')
|
| 342 |
+
.replace(/^\[ERROR\]\s*/i, '<span style="color:var(--orange)">✕ </span>');
|
| 343 |
+
return clean;
|
| 344 |
+
}).join('\n')
|
| 345 |
+
: 'No signals detected.';
|
| 346 |
+
|
| 347 |
+
const logEl = document.createElement('div');
|
| 348 |
+
logEl.className = 'adv-log';
|
| 349 |
+
logEl.style.gridColumn = '1 / -1';
|
| 350 |
+
logEl.innerHTML = `<div style="font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-3);margin-bottom:10px;">Diagnostic Signal Log</div>${logHtml}`;
|
| 351 |
+
container.appendChild(logEl);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
function makeAdvCard(title, itemsHtml) {
|
| 355 |
+
const card = document.createElement('div');
|
| 356 |
+
card.className = 'adv-card';
|
| 357 |
+
card.innerHTML = `<div class="adv-card-title">${title}</div><ul class="adv-list">${itemsHtml}</ul>`;
|
| 358 |
+
return card;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
// ── Toast Notification ────────────────────────────────────────────
|
| 362 |
+
function showToast(msg, type = 'info') {
|
| 363 |
+
// Remove existing toast if any
|
| 364 |
+
const old = document.getElementById('imgauth-toast');
|
| 365 |
+
if (old) old.remove();
|
| 366 |
+
|
| 367 |
+
const toast = document.createElement('div');
|
| 368 |
+
toast.id = 'imgauth-toast';
|
| 369 |
+
toast.style.cssText = `
|
| 370 |
+
position:fixed; bottom:28px; left:50%; transform:translateX(-50%);
|
| 371 |
+
background:${type === 'error' ? 'var(--red-bg)' : 'var(--surface-solid)'};
|
| 372 |
+
border:1px solid ${type === 'error' ? 'var(--red-border)' : 'var(--border)'};
|
| 373 |
+
color:var(--text); padding:12px 24px; border-radius:10px;
|
| 374 |
+
font-size:0.875rem; font-weight:600; font-family:var(--font);
|
| 375 |
+
box-shadow:var(--shadow-lg); z-index:9999;
|
| 376 |
+
animation:fadeInUp 0.25s ease;
|
| 377 |
+
`;
|
| 378 |
+
toast.textContent = msg;
|
| 379 |
+
document.body.appendChild(toast);
|
| 380 |
+
setTimeout(() => toast.remove(), 4000);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// Add toast animation
|
| 384 |
+
const toastStyle = document.createElement('style');
|
| 385 |
+
toastStyle.textContent = `
|
| 386 |
+
@keyframes fadeInUp {
|
| 387 |
+
from { opacity:0; transform:translateX(-50%) translateY(12px); }
|
| 388 |
+
to { opacity:1; transform:translateX(-50%) translateY(0); }
|
| 389 |
+
}
|
| 390 |
+
`;
|
| 391 |
+
document.head.appendChild(toastStyle);
|
| 392 |
+
|
| 393 |
+
// ── Utility ───────────────────────────────────────────────────────
|
| 394 |
+
function esc(str) {
|
| 395 |
+
if (!str) return '';
|
| 396 |
+
return String(str).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
| 397 |
+
}
|
public/.gitkeep
ADDED
|
File without changes
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.111.0
|
| 2 |
+
uvicorn==0.29.0
|
| 3 |
+
python-multipart==0.0.9
|
| 4 |
+
Pillow==10.3.0
|
| 5 |
+
transformers==4.41.0
|
| 6 |
+
torch==2.3.0
|
| 7 |
+
torchvision==0.18.0
|
| 8 |
+
numpy==1.26.4
|
| 9 |
+
opencv-python-headless==4.9.0.80
|
| 10 |
+
scipy==1.13.1
|
run.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess, sys
|
| 2 |
+
|
| 3 |
+
try:
|
| 4 |
+
subprocess.run([sys.executable, "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "5000"])
|
| 5 |
+
except KeyboardInterrupt:
|
| 6 |
+
print("\nImgAuth AI server stopped.")
|
| 7 |
+
sys.exit(0)
|
| 8 |
+
|