Spaces:
Running
Running
feat: updated detector using Ela fft and meta
Browse files- .env-example +0 -0
- .gitignore +0 -0
- Dockerfile +0 -0
- Procfile +0 -0
- README.md +0 -9
- app.py +4 -0
- config.py +0 -0
- docs/api_endpoints.md +0 -0
- docs/deployment.md +0 -0
- docs/detector/ELA.md +76 -0
- docs/detector/fft.md +141 -0
- docs/detector/meta.md +0 -0
- docs/detector/note-for-backend.md +92 -0
- docs/functions.md +0 -0
- docs/nestjs_integration.md +0 -0
- docs/security.md +0 -0
- docs/setup.md +0 -0
- docs/structure.md +0 -0
- features/image_classifier/__init__.py +0 -0
- features/image_classifier/controller.py +0 -0
- features/image_classifier/inferencer.py +0 -0
- features/image_classifier/model_loader.py +0 -0
- features/image_classifier/preprocess.py +0 -0
- features/image_classifier/routes.py +0 -0
- features/image_edit_detector/controller.py +49 -0
- features/image_edit_detector/detectors/ela.py +32 -0
- features/image_edit_detector/detectors/fft.py +40 -0
- features/image_edit_detector/detectors/metadata.py +82 -0
- features/image_edit_detector/preprocess.py +9 -0
- features/image_edit_detector/routes.py +53 -0
- features/nepali_text_classifier/__init__.py +0 -0
- features/nepali_text_classifier/controller.py +0 -0
- features/nepali_text_classifier/inferencer.py +0 -0
- features/nepali_text_classifier/model_loader.py +0 -0
- features/nepali_text_classifier/preprocess.py +0 -0
- features/nepali_text_classifier/routes.py +0 -0
- features/text_classifier/__init__.py +0 -0
- features/text_classifier/controller.py +0 -0
- features/text_classifier/inferencer.py +0 -0
- features/text_classifier/model_loader.py +0 -0
- features/text_classifier/preprocess.py +0 -0
- features/text_classifier/routes.py +0 -0
- readme.md +6 -32
- requirements.txt +0 -0
- test.md +31 -0
.env-example
CHANGED
File without changes
|
.gitignore
CHANGED
File without changes
|
Dockerfile
CHANGED
File without changes
|
Procfile
CHANGED
File without changes
|
README.md
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
---
|
2 |
-
title: Ai-Checker
|
3 |
-
emoji: π
|
4 |
-
colorFrom: yellow
|
5 |
-
colorTo: blue
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
---
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
@@ -7,6 +7,8 @@ from fastapi.responses import JSONResponse
|
|
7 |
from features.text_classifier.routes import router as text_classifier_router
|
8 |
from features.nepali_text_classifier.routes import router as nepali_text_classifier_router
|
9 |
from features.image_classifier.routes import router as image_classifier_router
|
|
|
|
|
10 |
from config import ACCESS_RATE
|
11 |
|
12 |
import requests
|
@@ -30,6 +32,8 @@ app.add_middleware(SlowAPIMiddleware)
|
|
30 |
app.include_router(text_classifier_router, prefix="/text")
|
31 |
app.include_router(nepali_text_classifier_router,prefix="/NP")
|
32 |
app.include_router(image_classifier_router,prefix="/AI-image")
|
|
|
|
|
33 |
@app.get("/")
|
34 |
@limiter.limit(ACCESS_RATE)
|
35 |
async def root(request: Request):
|
|
|
7 |
from features.text_classifier.routes import router as text_classifier_router
|
8 |
from features.nepali_text_classifier.routes import router as nepali_text_classifier_router
|
9 |
from features.image_classifier.routes import router as image_classifier_router
|
10 |
+
from features.image_edit_detector.routes import router as image_edit_detector_router
|
11 |
+
|
12 |
from config import ACCESS_RATE
|
13 |
|
14 |
import requests
|
|
|
32 |
app.include_router(text_classifier_router, prefix="/text")
|
33 |
app.include_router(nepali_text_classifier_router,prefix="/NP")
|
34 |
app.include_router(image_classifier_router,prefix="/AI-image")
|
35 |
+
app.include_router(image_edit_detector_router,prefix="/detect")
|
36 |
+
|
37 |
@app.get("/")
|
38 |
@limiter.limit(ACCESS_RATE)
|
39 |
async def root(request: Request):
|
config.py
CHANGED
File without changes
|
docs/api_endpoints.md
CHANGED
File without changes
|
docs/deployment.md
CHANGED
File without changes
|
docs/detector/ELA.md
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Error Level Analysis (ELA) Detector
|
2 |
+
|
3 |
+
This module provides a function to perform Error Level Analysis (ELA) on images to detect potential manipulations or edits.
|
4 |
+
|
5 |
+
|
6 |
+
## Function: `run_ela`
|
7 |
+
|
8 |
+
```python
|
9 |
+
def run_ela(image: Image.Image, quality: int = 90, threshold: int = 15) -> bool:
|
10 |
+
```
|
11 |
+
|
12 |
+
|
13 |
+
### Description
|
14 |
+
|
15 |
+
Error Level Analysis (ELA) works by recompressing an image at a specified JPEG quality level and comparing it to the original image. Differences between the two images reveal areas with inconsistent compression artifacts β often indicating image manipulation.
|
16 |
+
|
17 |
+
The function computes the maximum pixel difference across all color channels and uses a threshold to determine if the image is likely edited.
|
18 |
+
|
19 |
+
### Parameters
|
20 |
+
|
21 |
+
| Parameter | Type | Default | Description |
|
22 |
+
| ----------- | ----------- | ------- | ------------------------------------------------------------------------------------------- |
|
23 |
+
| `image` | `PIL.Image` | N/A | Input image in RGB mode to analyze. |
|
24 |
+
| `quality` | `int` | 90 | JPEG compression quality used for recompression during analysis (lower = more compression). |
|
25 |
+
| `threshold` | `int` | 15 | Pixel difference threshold to flag the image as edited. |
|
26 |
+
|
27 |
+
|
28 |
+
### Returns
|
29 |
+
|
30 |
+
`bool`
|
31 |
+
|
32 |
+
* `True` if the image is likely edited (max pixel difference > threshold).
|
33 |
+
* `False` if the image appears unedited.
|
34 |
+
|
35 |
+
|
36 |
+
### Usage Example
|
37 |
+
|
38 |
+
```python
|
39 |
+
from PIL import Image
|
40 |
+
from detectors.ela import run_ela
|
41 |
+
|
42 |
+
# Open and convert image to RGB
|
43 |
+
img = Image.open("example.jpg").convert("RGB")
|
44 |
+
|
45 |
+
# Run ELA detection
|
46 |
+
is_edited = run_ela(img, quality=90, threshold=15)
|
47 |
+
|
48 |
+
print("Image edited:", is_edited)
|
49 |
+
```
|
50 |
+
|
51 |
+
|
52 |
+
### Notes
|
53 |
+
|
54 |
+
* The input image **must** be in RGB mode for accurate analysis.
|
55 |
+
* ELA is a heuristic technique; combining it with other detection methods increases reliability.
|
56 |
+
* Visualizing the enhanced difference image can help identify edited regions (not returned by this function but possible to add).
|
57 |
+
|
58 |
+
|
59 |
+
### Installation
|
60 |
+
|
61 |
+
Make sure you have Pillow installed:
|
62 |
+
|
63 |
+
```bash
|
64 |
+
pip install pillow
|
65 |
+
```
|
66 |
+
|
67 |
+
|
68 |
+
### Running Locally
|
69 |
+
|
70 |
+
Just put the function in a notebook or script file and run it with your image. It works well for basic images.
|
71 |
+
|
72 |
+
|
73 |
+
### Developer
|
74 |
+
|
75 |
+
Pujan Neupane
|
76 |
+
|
docs/detector/fft.md
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# Fast Fourier Transform (FFT) Detector
|
3 |
+
|
4 |
+
```python
|
5 |
+
def run_fft(image: Image.Image, threshold: float = 0.92) -> bool:
|
6 |
+
```
|
7 |
+
|
8 |
+
## **Overview**
|
9 |
+
|
10 |
+
The `run_fft` function performs a frequency domain analysis on an image using the **Fast Fourier Transform (FFT)** to detect possible **AI generation or digital manipulation**. It leverages the fact that artificially generated or heavily edited images often exhibit a distinct high-frequency pattern.
|
11 |
+
|
12 |
+
---
|
13 |
+
|
14 |
+
## **Parameters**
|
15 |
+
|
16 |
+
| Parameter | Type | Description |
|
17 |
+
| ----------- | ----------------- | --------------------------------------------------------------------------------------- |
|
18 |
+
| `image` | `PIL.Image.Image` | Input image to analyze. It will be converted to grayscale and resized. |
|
19 |
+
| `threshold` | `float` | Proportion threshold of high-frequency components to flag the image. Default is `0.92`. |
|
20 |
+
|
21 |
+
---
|
22 |
+
|
23 |
+
## **Returns**
|
24 |
+
|
25 |
+
| Type | Description |
|
26 |
+
| ------ | ---------------------------------------------------------------------- |
|
27 |
+
| `bool` | `True` if image is likely AI-generated/manipulated; otherwise `False`. |
|
28 |
+
|
29 |
+
---
|
30 |
+
|
31 |
+
## **Step-by-Step Explanation**
|
32 |
+
|
33 |
+
### 1. **Grayscale Conversion**
|
34 |
+
|
35 |
+
All images are converted to grayscale:
|
36 |
+
|
37 |
+
```python
|
38 |
+
gray_image = image.convert("L")
|
39 |
+
```
|
40 |
+
|
41 |
+
### 2. **Resize**
|
42 |
+
|
43 |
+
The image is resized to a fixed $512 \times 512$ for uniformity:
|
44 |
+
|
45 |
+
```python
|
46 |
+
resized_image = gray_image.resize((512, 512))
|
47 |
+
```
|
48 |
+
|
49 |
+
### 3. **FFT Calculation**
|
50 |
+
|
51 |
+
Compute the 2D Discrete Fourier Transform:
|
52 |
+
|
53 |
+
$$
|
54 |
+
F(u, v) = \sum_{x=0}^{M-1} \sum_{y=0}^{N-1} f(x, y) \cdot e^{-2\pi i \left( \frac{ux}{M} + \frac{vy}{N} \right)}
|
55 |
+
$$
|
56 |
+
|
57 |
+
```python
|
58 |
+
fft_result = fft2(image_array)
|
59 |
+
```
|
60 |
+
|
61 |
+
### 4. **Shift Zero Frequency to Center**
|
62 |
+
|
63 |
+
Use `fftshift` to center the zero-frequency component:
|
64 |
+
|
65 |
+
```python
|
66 |
+
fft_shifted = fftshift(fft_result)
|
67 |
+
```
|
68 |
+
|
69 |
+
### 5. **Magnitude Spectrum**
|
70 |
+
|
71 |
+
$$
|
72 |
+
|F(u, v)| = \sqrt{\Re^2 + \Im^2}
|
73 |
+
$$
|
74 |
+
|
75 |
+
```python
|
76 |
+
magnitude_spectrum = np.abs(fft_shifted)
|
77 |
+
```
|
78 |
+
|
79 |
+
### 6. **Normalization**
|
80 |
+
|
81 |
+
Normalize the spectrum to avoid scale issues:
|
82 |
+
|
83 |
+
$$
|
84 |
+
\text{Normalized}(u,v) = \frac{|F(u,v)|}{\max(|F(u,v)|)}
|
85 |
+
$$
|
86 |
+
|
87 |
+
```python
|
88 |
+
normalized_spectrum = magnitude_spectrum / max_magnitude
|
89 |
+
```
|
90 |
+
|
91 |
+
### 7. **High-Frequency Detection**
|
92 |
+
|
93 |
+
High-frequency components are defined as:
|
94 |
+
|
95 |
+
$$
|
96 |
+
\text{Mask}(u,v) =
|
97 |
+
\begin{cases}
|
98 |
+
1 & \text{if } \text{Normalized}(u,v) > 0.5 \\
|
99 |
+
0 & \text{otherwise}
|
100 |
+
\end{cases}
|
101 |
+
$$
|
102 |
+
|
103 |
+
```python
|
104 |
+
high_freq_mask = normalized_spectrum > 0.5
|
105 |
+
```
|
106 |
+
|
107 |
+
### 8. **Proportion Calculation**
|
108 |
+
|
109 |
+
$$
|
110 |
+
\text{Ratio} = \frac{\sum \text{Mask}}{\text{Total pixels}}
|
111 |
+
$$
|
112 |
+
|
113 |
+
```python
|
114 |
+
high_freq_ratio = np.sum(high_freq_mask) / normalized_spectrum.size
|
115 |
+
```
|
116 |
+
|
117 |
+
### 9. **Threshold Decision**
|
118 |
+
|
119 |
+
If the ratio exceeds the threshold:
|
120 |
+
|
121 |
+
$$
|
122 |
+
\text{is\_fake} = (\text{Ratio} > \text{Threshold})
|
123 |
+
$$
|
124 |
+
|
125 |
+
```python
|
126 |
+
is_fake = high_freq_ratio > threshold
|
127 |
+
```
|
128 |
+
|
129 |
+
it is implemented in the api
|
130 |
+
|
131 |
+
### Running Locally
|
132 |
+
|
133 |
+
Just put the function in a notebook or script file and run it with your image. It works well for basic images.
|
134 |
+
|
135 |
+
|
136 |
+
### Worked By
|
137 |
+
|
138 |
+
Pujan Neupane
|
139 |
+
|
140 |
+
|
141 |
+
|
docs/detector/meta.md
ADDED
File without changes
|
docs/detector/note-for-backend.md
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# π¦API integration note
|
3 |
+
|
4 |
+
## Overview
|
5 |
+
|
6 |
+
This system integrates **three image forensics methods**β**ELA**, **FFT**, and **Metadata analysis**βinto a single detection pipeline to determine whether an image is AI-generated, manipulated, or authentic.
|
7 |
+
|
8 |
+
---
|
9 |
+
|
10 |
+
## π Detection Modules
|
11 |
+
|
12 |
+
### 1. **ELA (Error Level Analysis)**
|
13 |
+
|
14 |
+
* **Purpose:** Detects tampering or editing by analyzing compression error levels.
|
15 |
+
* **Accuracy:** β
*Most accurate method*
|
16 |
+
* **Performance:** β *Slowest method*
|
17 |
+
* **Output:** `True` (edited) or `False` (authentic)
|
18 |
+
|
19 |
+
### 2. **FFT (Fast Fourier Transform)**
|
20 |
+
|
21 |
+
* **Purpose:** Identifies high-frequency patterns typical of AI-generated images.
|
22 |
+
* **Accuracy:** β οΈ *Moderately accurate*
|
23 |
+
* **Performance:** β *Moderate to slow*
|
24 |
+
* **Output:** `True` (likely AI-generated) or `False` (authentic)
|
25 |
+
|
26 |
+
### 3. **Metadata Analysis**
|
27 |
+
|
28 |
+
* **Purpose:** Detects traces of AI tools or editors in image metadata or binary content.
|
29 |
+
* **Accuracy:** β οΈ *Fast but weaker signal*
|
30 |
+
* **Performance:** π *Fastest method*
|
31 |
+
* **Output:** One of:
|
32 |
+
|
33 |
+
* `"ai_generated"` β AI tool or generator identified
|
34 |
+
* `"edited"` β Edited using known software
|
35 |
+
* `"undetermined"` β No signature found
|
36 |
+
|
37 |
+
---
|
38 |
+
|
39 |
+
## π§© Integration Plan
|
40 |
+
|
41 |
+
### β Combine all three APIs into one unified endpoint:
|
42 |
+
|
43 |
+
```bash
|
44 |
+
POST /api/detect-image
|
45 |
+
```
|
46 |
+
|
47 |
+
### Input:
|
48 |
+
|
49 |
+
* `image`: Image file (binary, any format supported by Pillow)
|
50 |
+
|
51 |
+
### Output:
|
52 |
+
|
53 |
+
```json
|
54 |
+
{
|
55 |
+
"ela_result": true,
|
56 |
+
"fft_result": false,
|
57 |
+
"metadata_result": "ai_generated",
|
58 |
+
"final_decision": "ai_generated"
|
59 |
+
}
|
60 |
+
```
|
61 |
+
> NOTE:Optionally recommending a default logic (e.g., trust ELA > FFT > Metadata).
|
62 |
+
|
63 |
+
## Result implementation
|
64 |
+
| `ela_result` | `fft_result` | `metadata_result` | Suggested Final Decision | Notes |
|
65 |
+
| ------------ | ------------ | ----------------- | ------------------------ | ----------------------------------------------------------------------- |
|
66 |
+
| `true` | `true` | `"ai_generated"` | `ai_generated` | Strong evidence from all three modules |
|
67 |
+
| `true` | `false` | `"edited"` | `edited` | ELA confirms editing, no AI signals |
|
68 |
+
| `true` | `false` | `"undetermined"` | `edited` | ELA indicates manipulation |
|
69 |
+
| `false` | `true` | `"ai_generated"` | `ai_generated` | No edits, but strong AI frequency & metadata signature |
|
70 |
+
| `false` | `true` | `"undetermined"` | `possibly_ai_generated` | Weak metadata, but FFT indicates possible AI generation |
|
71 |
+
| `false` | `false` | `"ai_generated"` | `ai_generated` | Metadata alone shows AI use |
|
72 |
+
| `false` | `false` | `"edited"` | `possibly_edited` | Weak signalβmetadata shows editing but no structural or frequency signs |
|
73 |
+
| `false` | `false` | `"undetermined"` | `authentic` | No detectable manipulation or AI indicators |
|
74 |
+
|
75 |
+
|
76 |
+
### Decision Logic:
|
77 |
+
|
78 |
+
* Use **ELA** as the **primary indicator** for manipulation.
|
79 |
+
* Supplement with **FFT** and **Metadata** to improve reliability.
|
80 |
+
* Combine using a simple rule-based or voting system.
|
81 |
+
|
82 |
+
---
|
83 |
+
|
84 |
+
## βοΈ Performance Consideration
|
85 |
+
|
86 |
+
| Method | Speed | Strength |
|
87 |
+
| -------- | ----------- | -------------------- |
|
88 |
+
| ELA | β Slow | β
Highly accurate |
|
89 |
+
| FFT | β οΈ Moderate | β οΈ Somewhat reliable |
|
90 |
+
| Metadata | π Fast | β οΈ Low confidence |
|
91 |
+
|
92 |
+
> For high-throughput systems, consider running Metadata first and conditionally applying ELA/FFT if suspicious.
|
docs/functions.md
CHANGED
File without changes
|
docs/nestjs_integration.md
CHANGED
File without changes
|
docs/security.md
CHANGED
File without changes
|
docs/setup.md
CHANGED
File without changes
|
docs/structure.md
CHANGED
File without changes
|
features/image_classifier/__init__.py
CHANGED
File without changes
|
features/image_classifier/controller.py
CHANGED
File without changes
|
features/image_classifier/inferencer.py
CHANGED
File without changes
|
features/image_classifier/model_loader.py
CHANGED
File without changes
|
features/image_classifier/preprocess.py
CHANGED
File without changes
|
features/image_classifier/routes.py
CHANGED
File without changes
|
features/image_edit_detector/controller.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import Image
|
2 |
+
import io
|
3 |
+
from io import BytesIO
|
4 |
+
from .detectors.fft import run_fft
|
5 |
+
from .detectors.metadata import run_metadata
|
6 |
+
from .detectors.ela import run_ela
|
7 |
+
from .preprocess import preprocess_image
|
8 |
+
from fastapi import HTTPException,status,Depends
|
9 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
10 |
+
security=HTTPBearer()
|
11 |
+
import os
|
12 |
+
async def process_image_ela(image_bytes: bytes, quality: int=90):
|
13 |
+
image = Image.open(io.BytesIO(image_bytes))
|
14 |
+
|
15 |
+
if image.mode != "RGB":
|
16 |
+
image = image.convert("RGB")
|
17 |
+
|
18 |
+
compressed_image = preprocess_image(image, quality)
|
19 |
+
ela_result = run_ela(compressed_image, quality)
|
20 |
+
|
21 |
+
return {
|
22 |
+
"is_edited": ela_result,
|
23 |
+
"ela_score": ela_result
|
24 |
+
}
|
25 |
+
|
26 |
+
async def process_fft_image(image_bytes: bytes,threshold:float=0.95) -> dict:
|
27 |
+
image = Image.open(BytesIO(image_bytes)).convert("RGB")
|
28 |
+
result = run_fft(image,threshold)
|
29 |
+
return {"edited": bool(result)}
|
30 |
+
|
31 |
+
|
32 |
+
async def process_meta_image(image_bytes: bytes) -> dict:
|
33 |
+
try:
|
34 |
+
result = run_metadata(image_bytes)
|
35 |
+
return {"source": result} # e.g. "edited", "phone_capture", "unknown"
|
36 |
+
except Exception as e:
|
37 |
+
# Handle errors gracefully, return useful message or raise HTTPException if preferred
|
38 |
+
return {"error": str(e)}
|
39 |
+
|
40 |
+
|
41 |
+
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
42 |
+
token = credentials.credentials
|
43 |
+
expected_token = os.getenv("MY_SECRET_TOKEN")
|
44 |
+
if token != expected_token:
|
45 |
+
raise HTTPException(
|
46 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
47 |
+
detail="Invalid or expired token"
|
48 |
+
)
|
49 |
+
return token
|
features/image_edit_detector/detectors/ela.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import Image, ImageChops, ImageEnhance
|
2 |
+
import io
|
3 |
+
|
4 |
+
def run_ela(image: Image.Image, quality: int = 90, threshold: int = 15) -> bool:
|
5 |
+
"""
|
6 |
+
Perform Error Level Analysis to detect image manipulation.
|
7 |
+
|
8 |
+
Parameters:
|
9 |
+
image (PIL.Image): Input image (should be RGB).
|
10 |
+
quality (int): JPEG compression quality for ELA.
|
11 |
+
threshold (int): Maximum pixel difference threshold to classify as edited.
|
12 |
+
|
13 |
+
Returns:
|
14 |
+
bool: True if image appears edited, False otherwise.
|
15 |
+
"""
|
16 |
+
|
17 |
+
# Recompress the image into JPEG format in memory
|
18 |
+
buffer = io.BytesIO()
|
19 |
+
image.save(buffer, format='JPEG', quality=quality)
|
20 |
+
buffer.seek(0)
|
21 |
+
recompressed = Image.open(buffer)
|
22 |
+
|
23 |
+
# Compute the pixel-wise difference
|
24 |
+
diff = ImageChops.difference(image, recompressed)
|
25 |
+
extrema = diff.getextrema()
|
26 |
+
max_diff = max([ex[1] for ex in extrema])
|
27 |
+
|
28 |
+
# Enhance difference image for debug (not returned)
|
29 |
+
_ = ImageEnhance.Brightness(diff).enhance(10)
|
30 |
+
|
31 |
+
return max_diff > threshold
|
32 |
+
|
features/image_edit_detector/detectors/fft.py
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from PIL import Image
|
3 |
+
from scipy.fft import fft2, fftshift
|
4 |
+
|
5 |
+
|
6 |
+
def run_fft(image: Image.Image, threshold: float = 0.92) -> bool:
|
7 |
+
"""
|
8 |
+
Detects potential image manipulation or generation using FFT-based high-frequency analysis.
|
9 |
+
|
10 |
+
Parameters:
|
11 |
+
image (PIL.Image.Image): The input image.
|
12 |
+
threshold (float): Proportion of high-frequency components above which the image is flagged.
|
13 |
+
|
14 |
+
Returns:
|
15 |
+
bool: True if the image is likely AI-generated or manipulated, False otherwise.
|
16 |
+
"""
|
17 |
+
gray_image = image.convert("L")
|
18 |
+
|
19 |
+
resized_image = gray_image.resize((512, 512))
|
20 |
+
|
21 |
+
|
22 |
+
image_array = np.array(resized_image)
|
23 |
+
|
24 |
+
fft_result = fft2(image_array)
|
25 |
+
|
26 |
+
fft_shifted = fftshift(fft_result)
|
27 |
+
|
28 |
+
magnitude_spectrum = np.abs(fft_shifted)
|
29 |
+
max_magnitude = np.max(magnitude_spectrum)
|
30 |
+
if max_magnitude == 0:
|
31 |
+
return False # Avoid division by zero if image is blank
|
32 |
+
normalized_spectrum = magnitude_spectrum / max_magnitude
|
33 |
+
|
34 |
+
high_freq_mask = normalized_spectrum > 0.5
|
35 |
+
|
36 |
+
high_freq_ratio = np.sum(high_freq_mask) / normalized_spectrum.size
|
37 |
+
|
38 |
+
is_fake = high_freq_ratio > threshold
|
39 |
+
return is_fake
|
40 |
+
|
features/image_edit_detector/detectors/metadata.py
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import Image, UnidentifiedImageError
|
2 |
+
import io
|
3 |
+
|
4 |
+
# Common AI metadata identifiers in image files.
|
5 |
+
AI_INDICATORS = [
|
6 |
+
b'c2pa', b'claim_generator', b'claim_generator_info',
|
7 |
+
b'created_software_agent', b'actions.v2', b'assertions',
|
8 |
+
b'urn:c2pa', b'jumd', b'jumb', b'jumdcbor', b'jumdc2ma',
|
9 |
+
b'jumdc2as', b'jumdc2cl', b'cbor', b'convertedsfwareagent',b'c2pa.version',
|
10 |
+
b'c2pa.assertions', b'c2pa.actions',
|
11 |
+
b'c2pa.thumbnail', b'c2pa.signature', b'c2pa.manifest',
|
12 |
+
b'c2pa.manifest_store', b'c2pa.ingredient', b'c2pa.parent',
|
13 |
+
b'c2pa.provenance', b'c2pa.claim', b'c2pa.hash', b'c2pa.authority',
|
14 |
+
b'jumdc2pn', b'jumdrefs', b'jumdver', b'jumdmeta',
|
15 |
+
|
16 |
+
|
17 |
+
'midjourney'.encode('utf-8'),
|
18 |
+
'stable-diffusion'.encode('utf-8'),
|
19 |
+
'stable diffusion'.encode('utf-8'),
|
20 |
+
'stable_diffusion'.encode('utf-8'),
|
21 |
+
'artbreeder'.encode('utf-8'),
|
22 |
+
'runwayml'.encode('utf-8'),
|
23 |
+
'remix.ai'.encode('utf-8'),
|
24 |
+
'firefly'.encode('utf-8'),
|
25 |
+
'adobe_firefly'.encode('utf-8'),
|
26 |
+
|
27 |
+
# OpenAI / DALLΒ·E indicators (all encoded to bytes)
|
28 |
+
'openai'.encode('utf-8'),
|
29 |
+
'dalle'.encode('utf-8'),
|
30 |
+
'dalle2'.encode('utf-8'),
|
31 |
+
'DALL-E'.encode('utf-8'),
|
32 |
+
'DALLΒ·E'.encode('utf-8'),
|
33 |
+
'created_by: openai'.encode('utf-8'),
|
34 |
+
'tool: dalle'.encode('utf-8'),
|
35 |
+
'tool: dalle2'.encode('utf-8'),
|
36 |
+
'creator: openai'.encode('utf-8'),
|
37 |
+
'creator: dalle'.encode('utf-8'),
|
38 |
+
'openai.com'.encode('utf-8'),
|
39 |
+
'api.openai.com'.encode('utf-8'),
|
40 |
+
'openai_model'.encode('utf-8'),
|
41 |
+
'openai_gpt'.encode('utf-8'),
|
42 |
+
|
43 |
+
#Further possible AI-Generation Indicators
|
44 |
+
'generated_by'.encode('utf-8'),
|
45 |
+
'model_id'.encode('utf-8'),
|
46 |
+
'model_version'.encode('utf-8'),
|
47 |
+
'model_info'.encode('utf-8'),
|
48 |
+
'tool_name'.encode('utf-8'),
|
49 |
+
'tool_creator'.encode('utf-8'),
|
50 |
+
'tool_version'.encode('utf-8'),
|
51 |
+
'model_signature'.encode('utf-8'),
|
52 |
+
'ai_model'.encode('utf-8'),
|
53 |
+
'ai_tool'.encode('utf-8'),
|
54 |
+
'generator'.encode('utf-8'),
|
55 |
+
'generated_by_ai'.encode('utf-8'),
|
56 |
+
'ai_generated'.encode('utf-8'),
|
57 |
+
'ai_art'.encode('utf-8')
|
58 |
+
]
|
59 |
+
|
60 |
+
|
61 |
+
def run_metadata(image_bytes: bytes) -> str:
|
62 |
+
try:
|
63 |
+
img = Image.open(io.BytesIO(image_bytes))
|
64 |
+
img.load()
|
65 |
+
|
66 |
+
exif = img.getexif()
|
67 |
+
software = str(exif.get(305, "")).strip()
|
68 |
+
|
69 |
+
suspicious_editors = ["Photoshop", "GIMP", "Snapseed", "Pixlr", "VSCO", "Editor", "Adobe", "Luminar"]
|
70 |
+
|
71 |
+
if any(editor.lower() in software.lower() for editor in suspicious_editors):
|
72 |
+
return "edited"
|
73 |
+
|
74 |
+
if any(indicator in image_bytes for indicator in AI_INDICATORS):
|
75 |
+
return "ai_generated"
|
76 |
+
|
77 |
+
return "undetermined"
|
78 |
+
|
79 |
+
except UnidentifiedImageError:
|
80 |
+
return "error: invalid image format"
|
81 |
+
except Exception as e:
|
82 |
+
return f"error: {str(e)}"
|
features/image_edit_detector/preprocess.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import Image
|
2 |
+
import io
|
3 |
+
|
4 |
+
def preprocess_image(img: Image.Image, quality: int) -> Image.Image:
|
5 |
+
buffer = io.BytesIO()
|
6 |
+
img.save(buffer, format="JPEG", quality=quality)
|
7 |
+
buffer.seek(0)
|
8 |
+
return Image.open(buffer)
|
9 |
+
|
features/image_edit_detector/routes.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from slowapi import Limiter
|
2 |
+
from config import ACCESS_RATE
|
3 |
+
from fastapi import APIRouter, File, Request, Depends, HTTPException, UploadFile
|
4 |
+
from fastapi.security import HTTPBearer
|
5 |
+
from slowapi import Limiter
|
6 |
+
from slowapi.util import get_remote_address
|
7 |
+
from io import BytesIO
|
8 |
+
from .controller import process_image_ela , verify_token,process_fft_image, process_meta_image
|
9 |
+
import requests
|
10 |
+
router = APIRouter()
|
11 |
+
limiter = Limiter(key_func=get_remote_address)
|
12 |
+
security = HTTPBearer()
|
13 |
+
|
14 |
+
|
15 |
+
|
16 |
+
@router.post("/ela")
|
17 |
+
@limiter.limit(ACCESS_RATE)
|
18 |
+
async def detect_ela(request:Request,file: UploadFile = File(...), quality: int = 90 ,token: str = Depends(verify_token)):
|
19 |
+
# Check file extension
|
20 |
+
allowed_types = ["image/jpeg", "image/png"]
|
21 |
+
|
22 |
+
if file.content_type not in allowed_types:
|
23 |
+
raise HTTPException(
|
24 |
+
status_code=400,
|
25 |
+
detail="Unsupported file type. Only JPEG and PNG images are allowed."
|
26 |
+
)
|
27 |
+
|
28 |
+
content = await file.read()
|
29 |
+
result = await process_image_ela(content, quality)
|
30 |
+
return result
|
31 |
+
|
32 |
+
@router.post("/fft")
|
33 |
+
@limiter.limit(ACCESS_RATE)
|
34 |
+
async def detect_fft(request:Request,file:UploadFile =File(...),threshold:float=0.95,token:str=Depends(verify_token)):
|
35 |
+
if file.content_type not in ["image/jpeg", "image/png"]:
|
36 |
+
raise HTTPException(status_code=400, detail="Unsupported image type.")
|
37 |
+
|
38 |
+
content = await file.read()
|
39 |
+
result = await process_fft_image(content,threshold)
|
40 |
+
return result
|
41 |
+
|
42 |
+
@router.post("/meta")
|
43 |
+
@limiter.limit(ACCESS_RATE)
|
44 |
+
async def detect_meta(request:Request,file:UploadFile=File(...),token:str=Depends(verify_token)):
|
45 |
+
if file.content_type not in ["image/jpeg", "image/png"]:
|
46 |
+
raise HTTPException(status_code=400, detail="Unsupported image type.")
|
47 |
+
content = await file.read()
|
48 |
+
result = await process_meta_image(content)
|
49 |
+
return result
|
50 |
+
@router.post("/health")
|
51 |
+
@limiter.limit(ACCESS_RATE)
|
52 |
+
def heath(request:Request):
|
53 |
+
return {"status":"ok"}
|
features/nepali_text_classifier/__init__.py
CHANGED
File without changes
|
features/nepali_text_classifier/controller.py
CHANGED
File without changes
|
features/nepali_text_classifier/inferencer.py
CHANGED
File without changes
|
features/nepali_text_classifier/model_loader.py
CHANGED
File without changes
|
features/nepali_text_classifier/preprocess.py
CHANGED
File without changes
|
features/nepali_text_classifier/routes.py
CHANGED
File without changes
|
features/text_classifier/__init__.py
CHANGED
File without changes
|
features/text_classifier/controller.py
CHANGED
File without changes
|
features/text_classifier/inferencer.py
CHANGED
File without changes
|
features/text_classifier/model_loader.py
CHANGED
File without changes
|
features/text_classifier/preprocess.py
CHANGED
File without changes
|
features/text_classifier/routes.py
CHANGED
File without changes
|
readme.md
CHANGED
@@ -1,35 +1,9 @@
|
|
1 |
-
# π FastAPI AI Detector
|
2 |
-
|
3 |
-
A production-ready FastAPI app for detecting AI vs. human-written text in English and Nepali. It uses GPT-2 and SentencePiece-based models, with Bearer token security.
|
4 |
-
|
5 |
-
## π Documentation
|
6 |
-
|
7 |
-
- [Project Structure](docs/structure.md)
|
8 |
-
- [API Endpoints](docs/api_endpoints.md)
|
9 |
-
- [Setup & Installation](docs/setup.md)
|
10 |
-
- [Deployment](docs/deployment.md)
|
11 |
-
- [Security](docs/security.md)
|
12 |
-
- [NestJS Integration](docs/nestjs_integration.md)
|
13 |
-
- [Core Functions](docs/functions.md)
|
14 |
-
|
15 |
-
## β‘ Quick Start
|
16 |
-
```bash
|
17 |
-
uvicorn app:app --host 0.0.0.0 --port 8000
|
18 |
-
```
|
19 |
-
## π Deployment
|
20 |
-
|
21 |
-
- **Local**: Use `uvicorn` as above.
|
22 |
-
- **Railway/Heroku**: Use the provided `Procfile`.
|
23 |
-
- **Hugging Face Spaces**: Use the `Dockerfile` for container deployment.
|
24 |
-
|
25 |
---
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
- **For security**: Avoid committing `.env` to public repos.
|
33 |
-
|
34 |
---
|
35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: Ai-Checker
|
3 |
+
emoji: π
|
4 |
+
colorFrom: yellow
|
5 |
+
colorTo: blue
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
|
|
|
|
8 |
---
|
9 |
|
requirements.txt
CHANGED
File without changes
|
test.md
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
**Update: Edited & AI-Generated Content Detection β Project Plan**
|
3 |
+
|
4 |
+
### π Phase 1: Rule-Based Image Detection (In Progress)
|
5 |
+
|
6 |
+
We're implementing three core techniques to individually flag edited or AI-generated images:
|
7 |
+
|
8 |
+
* **ELA (Error Level Analysis):** Highlights inconsistencies via JPEG recompression.
|
9 |
+
* **FFT (Frequency Analysis):** Uses 2D Fourier Transform to detect unnatural image frequency patterns.
|
10 |
+
* **Metadata Analysis:** Parses EXIF data to catch clues like editing software tags.
|
11 |
+
|
12 |
+
These give us visual + interpretable results for each image, and currently offer \~60β70% accuracy on typical AI-edited content.
|
13 |
+
|
14 |
+
---
|
15 |
+
|
16 |
+
### Phase 2: AI vs Human Detection System (Coming Soon)
|
17 |
+
|
18 |
+
**Goal:** Build an AI model that classifies whether content is AI- or human-made β initially focusing on **images**, and later expanding to **text**.
|
19 |
+
|
20 |
+
**Data Strategy:**
|
21 |
+
|
22 |
+
* Scraping large volumes of recent AI-gen images (e.g. SDXL, Gibbli, MidJourney).
|
23 |
+
* Balancing with high-quality human images.
|
24 |
+
|
25 |
+
**Model Plan:**
|
26 |
+
|
27 |
+
* Use ELA, FFT, and metadata as feature extractors.
|
28 |
+
* Feed these into a CNN or ensemble model.
|
29 |
+
* Later, unify into a full web-based platform (upload β get AI/human probability).
|
30 |
+
|
31 |
+
|