Skip to content

Repository files navigation

aidetect

CI coverage

Lightweight, CPU-only detector that scores the probability that an image was generated by AI — no GPU, no PyTorch/TensorFlow. The goal is a cheap baseline that runs anywhere: the default pipeline is pure classical image forensics (NumPy/SciPy), and an optional, still-CPU CLIP probe (ONNX) adds a first lightweight neural signal for better generalization across unseen image sources — off by default.

Under the hood it is a reproducible, evolvable calibration pipeline: pluggable labeled data sources, training provenance (git SHA + library versions + data manifest) baked into every artifact, seeded determinism, and honest cross-generator / degradation benchmarks. Swapping or stacking a stronger model is just re-running the pipeline — so richer neural detectors can be folded in as they become cheap enough to run on CPU. Treat the score as a probabilistic aid, not a verdict (see Known limitations).

How it works

Two layers:

  1. Features (aidetect.features) — pure NumPy/SciPy/Pillow functions, each turning an image into a scalar:

    • Azimuthally-averaged power spectrum (high-frequency ratio, log-log slope, residual spikes) — the strongest signal; generators that upsample via up-convolution fail to reproduce the high-frequency tail of natural images (Durall et al., CVPR 2020).
    • FFT magnitude uniformity.
    • Error Level Analysis and 8×8 JPEG block variance (low weight: these reflect compression history more than generative origin).
    • High-frequency (Laplacian) energy.
    • Mean per-channel color entropy.
    • Texture patch-variance consistency.
    • Chromatic aberration (channel edge correlation).
    • EXIF/C2PA AI-provenance metadata flag.
    • SRM high-pass noise-residual statistics (std and excess kurtosis) — real photos carry rich, near-Gaussian sensor noise; many generators leave a depleted or distorted residual (Fridrich & Kodovský, 2012).
    • Patch-based spatial-consistency dispersion of the residual and Laplacian maps — camera noise is spatially uniform, while synthesis can leave high-frequency energy unevenly distributed across the image.

    The feature vector is a fixed, append-only sequence of 15 scalars (FEATURE_NAMES in features/extract.py).

  2. Scoring (aidetect.scoring):

    • default — hand-set, uncalibrated weights. Works with zero setup, but scores are crude. Clearly labeled as such.
    • calibrated — a StandardScaler + LogisticRegression pipeline loaded from models/calibrated.joblib. Used automatically when present.
    • clip (optional, off by default) — a logistic probe on CLIP image embeddings (ViT-B/32 vision tower, ONNX, CPU). Generalizes far better across unseen real-image domains than the forensic features. Enable with --clip / AIDETECT_CLIP=1; see Optional CLIP probe.

Install

pip install ".[api,calibrate,dev]"

Core dependencies are numpy, scipy, Pillow, piexif. The API and calibration dependencies are optional extras. There is no torch/torchvision. The optional clip extra (pip install ".[clip]") adds onnxruntime for the CLIP probe — still CPU-only, and off by default.

Usage

CLI

aidetect path/to/image.png            # human-readable report
aidetect path/to/image.png --json     # machine-readable JSON
aidetect path/to/image.png --verbose  # per-feature values and contributions
aidetect path/to/image.png --model models/calibrated.joblib
aidetect path/to/image.png --clip     # blend in the optional CLIP probe (off by default)
aidetect --model-info                 # loaded model's metrics + provenance (no image)
aidetect --model-info --json          # the same, machine-readable

Python API

from aidetect import analyze

result = analyze("path/to/image.png")
print(result.ai_score, result.probability, result.verdict, result.mode)

Each result carries both a precise probability in [0, 1] and a coarse integer ai_score from 1 to 100 (min(100, max(1, round(probability * 100)))). probability is the authoritative value; ai_score is a display convenience, and its floor of 1 means "very unlikely", not "impossible" — use probability when you need precision.

HTTP API

uvicorn aidetect.api:app --host 0.0.0.0 --port 8000
  • GET / → a minimal browser UI for manually uploading one image and viewing the verdict/score (drag-and-drop, image preview, raw JSON). Open http://localhost:8000/.
  • GET /health{"status": "ok", "mode": ..., "version": ..., "max_concurrency": N}
  • GET /model{"mode": ..., "model": {...}} — the loaded model's metrics, data manifest and training provenance (git SHA, library versions); model is null in default mode. Excludes the pickled estimators.
  • POST /analyze (multipart upload field file) → JSON analysis result
  • POST /analyze/batch (repeated multipart field files) → {"results": [...]}, one entry per image (1:1, order-preserving). A bad image yields a per-item {"filename", "error"} entry instead of failing the whole batch.
curl -F "file=@image.png" http://localhost:8000/analyze

# Batch: repeat the `files` field once per image.
curl -F "files=@a.png" -F "files=@b.png" http://localhost:8000/analyze/batch

Concurrency and limits

The analysis is CPU-bound and runs in a worker thread so the event loop stays free. A global concurrency cap provides backpressure, and oversized uploads are rejected before decoding. Responses:

  • 413 — upload exceeds the byte limit or pixel budget, or a batch has too many files. Images are not downscaled, since that would distort the frequency-domain signal.
  • 429 — all analysis slots are busy, or the per-IP rate limit was exceeded; retry shortly (Retry-After header).

Tunables (environment variables):

Variable Default Meaning
AIDETECT_MAX_UPLOAD_BYTES 10485760 (10 MiB) Max upload size
AIDETECT_MAX_PIXELS 24000000 (24 MP) Max width×height
AIDETECT_MAX_CONCURRENCY 4 Max simultaneous analyses (per worker)
AIDETECT_MAX_BATCH 16 Max files per /analyze/batch request
AIDETECT_RATE_LIMIT_PER_MIN 0 (off) Per-IP requests/minute on the analyze endpoints
AIDETECT_WORKERS 1 uvicorn worker processes (Docker)

The rate limit is in-process and best-effort (single worker), keyed on the direct client IP — X-Forwarded-For is not trusted, so behind a reverse proxy it limits by the proxy's address.

Total simultaneous analyses = AIDETECT_WORKERS × AIDETECT_MAX_CONCURRENCY. Keep AIDETECT_WORKERS=1 for a single, predictable global cap; raise it to use more CPU cores at the cost of multiplying the effective cap.

Docker

docker build -t aidetect .

# Serve the API (default command):
docker run --rm -p 8000:8000 aidetect

# Or with compose (mounts ./models so a calibrated model is picked up):
docker compose up --build

# Run the CLI inside the container against a mounted file:
docker run --rm -v "$PWD:/data" aidetect aidetect /data/image.png

Calibration (optional)

The repository ships a calibrated model at models/calibrated.joblib (calibrated on CIFAKE), so the CLI, API and Docker image run in calibrated mode out of the box. The model loads only if scikit-learn/joblib are installed (the calibrate extra; the Docker image includes them); if loading fails for any reason — including a scikit-learn version mismatch — the detector falls back to default mode rather than erroring. Re-run the command below to regenerate it for your environment.

To (re)calibrate on a labeled dataset, install the optional extra (pip install ".[calibrate]", which pulls in scikit-learn, joblib and Hugging Face datasets).

Start with CIFAKE, downloaded automatically from Hugging Face:

python -m aidetect.calibrate.train --dataset cifake_hf --subset 2000

Or point at a local CIFAKE-style folder (REAL/ and FAKE/ subdirectories):

python -m aidetect.calibrate.train --dataset folder --data data/cifake/train

What this does:

  1. Splits at the source-image level first (no augmented variant leaks across the train/test boundary).
  2. Extracts features; the training set is augmented (mild JPEG recompression, resize and blur) for robustness, while the holdout stays clean.
  3. Fits StandardScaler + LogisticRegression, then calibrates probabilities with CalibratedClassifierCV.
  4. Reports accuracy, ROC-AUC, precision/recall/F1, confusion matrix and Brier score (calibration quality), and saves models/calibrated.joblib (kilobytes).

The detector, CLI and API then run in calibrated mode automatically.

Useful flags:

  • --no-augment — disable augmentation (use to compare clean vs augmented metrics).
  • --aug-copies N — augmented variants per training image (default 1, i.e. original
    • 1 variant, which doubles the training set).
  • --test-dataset <name> / --test-data <dir> — evaluate on a separate set (useful later for cross-generator testing).

Caveat: CIFAKE images are 32×32 and from one generator (Stable Diffusion 1.4), so a model calibrated only on it generalizes less to high-resolution Midjourney/DALL·E outputs. --dataset is repeatable to combine several sources in one run (e.g. --dataset defactify_hf --dataset coco_real_hf), with train-fold balancing (--balance) and cross-source real dedup (--dedup-reals); see docs/datasets.md for the available sources. The generalization study that motivated the diverse-data and CLIP work — including the finding that forensic features struggle to cross real-image domains — is in docs/roadmap-2.md.

Optional CLIP probe

The forensic features are cheap but partly encode real-image-domain statistics (resolution, compression, sensor noise), so a model trained on one real domain can misrank photos from another. On a held-out cross-dataset benchmark the heuristic collapses to ROC-AUC ≈ 0.37 (worse than chance), while a linear probe on CLIP image embeddings stays at ≈ 0.84 (≈ 0.98 in-domain). CLIP is the more robust path, at the cost of an ONNX model download and heavier per-image compute.

It is off by default, keeping the lightweight core intact:

  • Install the extra: pip install ".[clip]" (adds onnxruntime; still CPU-only, no torch).
  • Enable per call with aidetect image.png --clip, or set AIDETECT_CLIP=1 (also honored by the API).
  • A small probe ships at models/clip_probe.joblib; the CLIP ViT-B/32 vision ONNX (~350 MB) is fetched on first use into the Hugging Face cache. Point AIDETECT_CLIP_DIR at a pre-placed model.onnx for offline use.
  • If onnxruntime, the probe, or the model are missing, the detector silently falls back to heuristic-only — enabling --clip never breaks a run.

When active, CLIP carries the score (the heuristic probability is still reported as clip_probability vs the blended probability). Rebuild the probe with python -m aidetect.calibrate.clip_calibrate (a CLIP probe plus a multi-domain sigmoid recalibration); details and benchmarks in docs/roadmap-2.md.

Known limitations

This is a probabilistic aid, not a reliable classifier. The two detectors have opposite, complementary failure modes, and on some inputs neither is right:

  • The CLIP probe is blind to photorealistic AI of people. It works on semantic CLIP embeddings (what is in the image, not how it was made), so a modern photorealistic render of a person (e.g. a CivitAI full-body portrait) lands deep in "real" space and is under-scored. CLIP's strength is the opposite case: it does not false-flag genuine photos when the real domain shifts.
  • The heuristic forensic features over-flag heavily processed real photos. They key on generation fingerprints (high-frequency residuals, frequency falloff), which a smooth, recompressed real photo (WebP, phone "beauty" processing) can mimic — so a real image can score as AI.
  • Some cases are unseparable with these signals. We have observed a real photo that scores higher than a genuinely AI image on both the heuristic and the CLIP signal at once. No combination of the two (max, average, or a learned blend) can label both correctly — this is a discriminative ceiling of CLIP ViT-B/32 plus classical forensic features against modern generators, not a tuning bug.

Practically: the detector is strongest on stylized or older-Stable-Diffusion content and weakest on photorealistic AI people. The training fakes currently cover SD/MJ/DALL·E (defactify_hf, julienlucas_hf); the planned lever for modern coverage is adding fakes from the actual threat distribution (CivitAI / Flux / Pony photorealistic generators). Treat the probability as a signal to weigh, not a verdict.

Tests

pytest tests/
pytest tests/ --cov=aidetect --cov-report=term-missing   # with coverage
mypy                                                      # static type check

License

Released under the MIT License — Copyright (c) 2026 André Zaiats.

About

Cheap, CPU-only AI-image detector (no GPU/PyTorch). A reproducible classical image-forensics pipeline with an optional, still-CPU CLIP (ONNX) neural probe — calibratable, benchmarked, and built to fold in stronger models as they get cheap. CLI, FastAPI, Docker.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages