suyash-77 commited on
Commit
a02f72f
·
verified ·
1 Parent(s): 38fc70e

Upload 9 files

Browse files

Initial deployment: DeepGuard AI Forensics Backend

Files changed (9) hide show
  1. Dockerfile +26 -0
  2. LICENSE +13 -0
  3. download_model.py +93 -0
  4. ela.py +51 -0
  5. heatmap.py +220 -0
  6. inference.py +169 -0
  7. main.py +217 -0
  8. metadata.py +103 -0
  9. requirements.txt +10 -0
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies required for OpenCV and ONNX
7
+ RUN apt-get update && apt-get install -y \
8
+ libgl1-mesa-glx \
9
+ libglib2.0-0 \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements and install
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy all backend files into the container
17
+ COPY . .
18
+
19
+ # Download the ONNX model inside the cloud container
20
+ RUN python download_model.py
21
+
22
+ # Expose the port FastAPI runs on
23
+ EXPOSE 8000
24
+
25
+ # Command to run the application (Hugging Face Spaces expects 0.0.0.0)
26
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
LICENSE ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2026. All Rights Reserved.
2
+
3
+ This software and associated documentation files (the "Software") are proprietary.
4
+
5
+ You are permitted to view the source code for educational or portfolio evaluation purposes.
6
+
7
+ However, you may NOT use, copy, modify, distribute, or run this Software without explicit, written permission from the author.
8
+
9
+ If permission is granted by the author, you MUST give full credit and attribution to the original author in any public display or distribution of the work.
10
+
11
+ To request permission for use, please contact the author directly.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE.
download_model.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepGuard — Model Download Script
3
+ Downloads the ViT-based deepfake detection ONNX model from Hugging Face Hub.
4
+
5
+ Usage:
6
+ python download_model.py
7
+
8
+ Model: onnx-community/Deep-Fake-Detector-v2-Model-ONNX
9
+ - Architecture: google/vit-base-patch16-224-in21k (fine-tuned)
10
+ - Task: Binary classification — Realism vs. Deepfake
11
+ - Labels: {0: "Realism", 1: "Deepfake"}
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import hashlib
17
+
18
+ MODELS_DIR = os.path.join(os.path.dirname(__file__), "models")
19
+ MODEL_DEST = os.path.join(MODELS_DIR, "deepfake_vit.onnx")
20
+
21
+ REPO_ID = "onnx-community/Deep-Fake-Detector-v2-Model-ONNX"
22
+ FILENAME = "onnx/model.onnx" # Path inside the HF repo
23
+
24
+
25
+ def sha256(path: str) -> str:
26
+ h = hashlib.sha256()
27
+ with open(path, "rb") as f:
28
+ for chunk in iter(lambda: f.read(65536), b""):
29
+ h.update(chunk)
30
+ return h.hexdigest()
31
+
32
+
33
+ def download():
34
+ os.makedirs(MODELS_DIR, exist_ok=True)
35
+
36
+ if os.path.exists(MODEL_DEST):
37
+ size_mb = os.path.getsize(MODEL_DEST) / (1024 * 1024)
38
+ print(f"[DeepGuard] Model already exists ({size_mb:.1f} MB): {MODEL_DEST}")
39
+ print(f"[DeepGuard] SHA-256: {sha256(MODEL_DEST)}")
40
+ print("[DeepGuard] Delete the file and re-run this script to force re-download.")
41
+ return
42
+
43
+ print(f"[DeepGuard] Downloading model from Hugging Face Hub...")
44
+ print(f" Repo: {REPO_ID}")
45
+ print(f" File: {FILENAME}")
46
+ print(f" Target: {MODEL_DEST}")
47
+ print()
48
+
49
+ try:
50
+ from huggingface_hub import hf_hub_download
51
+
52
+ tmp_path = hf_hub_download(
53
+ repo_id=REPO_ID,
54
+ filename=FILENAME,
55
+ cache_dir=MODELS_DIR,
56
+ local_dir=MODELS_DIR,
57
+ local_dir_use_symlinks=False,
58
+ )
59
+
60
+ # hf_hub_download writes to local_dir/<filename>
61
+ # Move if needed
62
+ expected_local = os.path.join(MODELS_DIR, "onnx", "model.onnx")
63
+ if os.path.exists(expected_local) and not os.path.exists(MODEL_DEST):
64
+ import shutil
65
+ shutil.move(expected_local, MODEL_DEST)
66
+
67
+ if not os.path.exists(MODEL_DEST):
68
+ # Try symlink/copy from tmp_path
69
+ import shutil
70
+ shutil.copy2(tmp_path, MODEL_DEST)
71
+
72
+ size_mb = os.path.getsize(MODEL_DEST) / (1024 * 1024)
73
+ checksum = sha256(MODEL_DEST)
74
+ print(f"\n[DeepGuard] [OK] Download complete!")
75
+ print(f" Size: {size_mb:.1f} MB")
76
+ print(f" SHA-256: {checksum}")
77
+ print(f" Path: {MODEL_DEST}")
78
+
79
+ except ImportError:
80
+ print("[ERROR] huggingface_hub not installed. Run: pip install huggingface_hub")
81
+ sys.exit(1)
82
+ except Exception as e:
83
+ print(f"[ERROR] Download failed: {e}")
84
+ print()
85
+ print("Manual download instructions:")
86
+ print(f" 1. Visit: https://huggingface.co/{REPO_ID}/tree/main/onnx")
87
+ print(f" 2. Download 'model.onnx'")
88
+ print(f" 3. Place it at: {MODEL_DEST}")
89
+ sys.exit(1)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ download()
ela.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import base64
3
+ from PIL import Image, ImageChops, ImageEnhance
4
+
5
+ def generate_ela(image: Image.Image, quality: int = 90, scale: float = 15.0) -> str:
6
+ """
7
+ Performs Error Level Analysis (ELA) on an image to highlight manipulated regions.
8
+ Saves the image as a temporary JPEG at a specific quality level and compares
9
+ it with the original to find compression differences.
10
+
11
+ Args:
12
+ image: Original PIL Image.
13
+ quality: JPEG compression quality for the resaved image (default 90).
14
+ scale: Brightness multiplier to make the differences visible (default 15.0).
15
+
16
+ Returns:
17
+ Base64-encoded string of the ELA image.
18
+ """
19
+ try:
20
+ # Convert to RGB if necessary
21
+ if image.mode != "RGB":
22
+ image = image.convert("RGB")
23
+
24
+ # 1. Resave the image in memory at a specific quality
25
+ temp_buffer = io.BytesIO()
26
+ image.save(temp_buffer, "JPEG", quality=quality)
27
+ temp_buffer.seek(0)
28
+
29
+ # 2. Open the resaved image
30
+ resaved_img = Image.open(temp_buffer)
31
+
32
+ # 3. Calculate the absolute difference between original and resaved
33
+ # Manipulated areas will stand out because they compress differently
34
+ ela_img = ImageChops.difference(image, resaved_img)
35
+
36
+ # 4. Enhance the difference (brightness) so it's visible to the human eye
37
+ enhancer = ImageEnhance.Brightness(ela_img)
38
+ ela_enhanced = enhancer.enhance(scale)
39
+
40
+ # 5. Convert to Base64 for the frontend
41
+ out_buffer = io.BytesIO()
42
+ ela_enhanced.save(out_buffer, format="JPEG", quality=85)
43
+ out_buffer.seek(0)
44
+ base64_str = base64.b64encode(out_buffer.read()).decode("utf-8")
45
+
46
+ return f"data:image/jpeg;base64,{base64_str}"
47
+
48
+ except Exception as e:
49
+ print(f"[DeepGuard] ELA Generation Error: {e}")
50
+ # Return empty string on failure
51
+ return ""
heatmap.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepGuard — Heatmap Generation Module
3
+
4
+ Strategy:
5
+ 1. PRIMARY: Attention Rollout — extract multi-head attention matrices from
6
+ ONNX intermediate outputs and roll them up through all layers.
7
+ 2. FALLBACK: Frequency Anomaly + Gradient Saliency — if attention weights
8
+ are not exported, compute a forensically meaningful heatmap
9
+ using DCT frequency analysis and Sobel edge gradients.
10
+ (Pure NumPy, <10ms, no additional inference passes.)
11
+ """
12
+
13
+ import io
14
+ import numpy as np
15
+ from PIL import Image
16
+ from scipy.ndimage import gaussian_filter
17
+ import base64
18
+ from typing import Optional
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Public entry point
23
+ # ---------------------------------------------------------------------------
24
+
25
+ def generate_heatmap(
26
+ image: Image.Image,
27
+ output_dict: dict,
28
+ confidence_score: float,
29
+ ) -> str:
30
+ """
31
+ Generate a transparent red/yellow heatmap overlay.
32
+
33
+ Args:
34
+ image: Original PIL image (any size).
35
+ output_dict: Raw ONNX output dict {name: ndarray}.
36
+ confidence_score: Model fake probability [0, 1].
37
+
38
+ Returns:
39
+ data URI string: "data:image/png;base64,..."
40
+ """
41
+ img224 = image.convert("RGB").resize((224, 224), Image.BILINEAR)
42
+ img_arr = np.array(img224, dtype=np.float32)
43
+
44
+ # Try attention rollout first
45
+ attn_keys = [
46
+ k for k in output_dict
47
+ if "attn" in k.lower() or "attention" in k.lower()
48
+ ]
49
+
50
+ heat_map = None
51
+ if attn_keys:
52
+ heat_map = _attention_rollout(output_dict, attn_keys)
53
+
54
+ if heat_map is None:
55
+ heat_map = _frequency_saliency(img_arr, confidence_score)
56
+
57
+ overlay = _apply_overlay(img_arr, heat_map)
58
+ return _encode_png(overlay)
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Strategy 1: Attention Rollout
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def _attention_rollout(output_dict: dict, attn_keys: list) -> Optional[np.ndarray]:
66
+ """
67
+ Roll up multi-head attention matrices across all transformer layers.
68
+ Returns a normalized (224, 224) float32 array or None on failure.
69
+ """
70
+ try:
71
+ # Sort keys to ensure layer order (layer_0, layer_1, ...)
72
+ attn_keys_sorted = sorted(attn_keys)
73
+ rollout = None
74
+
75
+ for key in attn_keys_sorted:
76
+ attn = output_dict[key] # Expected shape: (1, heads, seq_len, seq_len)
77
+ if attn.ndim != 4:
78
+ continue
79
+ attn = attn.squeeze(0) # (heads, seq_len, seq_len)
80
+ attn = attn.mean(axis=0) # Average heads → (seq_len, seq_len)
81
+
82
+ # Add residual identity (attention rollout formula)
83
+ identity = np.eye(attn.shape[0], dtype=np.float32)
84
+ attn = 0.5 * attn + 0.5 * identity
85
+ attn = attn / (attn.sum(axis=-1, keepdims=True) + 1e-8)
86
+
87
+ rollout = attn if rollout is None else np.matmul(rollout, attn)
88
+
89
+ if rollout is None:
90
+ return None
91
+
92
+ # Row 0 = CLS token → attends to all patch tokens
93
+ cls_attn = rollout[0, 1:] # Drop CLS itself → (num_patches,)
94
+ num_patches = cls_attn.shape[0]
95
+ side = int(np.sqrt(num_patches)) # 14 for ViT-base-patch16
96
+
97
+ if side * side != num_patches:
98
+ return None
99
+
100
+ patch_map = cls_attn.reshape(side, side)
101
+ patch_map = (patch_map - patch_map.min()) / (patch_map.max() - patch_map.min() + 1e-8)
102
+
103
+ # Upsample to 224×224
104
+ heat = _upsample(patch_map, 224, 224)
105
+ heat = gaussian_filter(heat, sigma=8)
106
+ return _normalize(heat)
107
+
108
+ except Exception:
109
+ return None
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Strategy 2: Frequency Anomaly + Sobel Saliency (pure NumPy fallback)
114
+ # ---------------------------------------------------------------------------
115
+
116
+ def _frequency_saliency(img_arr: np.ndarray, confidence_score: float) -> np.ndarray:
117
+ """
118
+ Generate a heatmap from:
119
+ - DCT/FFT frequency anomalies (AI images have characteristic frequency patterns)
120
+ - Sobel gradient magnitude (AI fails at object/background boundaries)
121
+
122
+ Both signals are combined and weighted by the confidence score.
123
+ """
124
+ gray = 0.299 * img_arr[:, :, 0] + 0.587 * img_arr[:, :, 1] + 0.114 * img_arr[:, :, 2]
125
+ gray_norm = gray / 255.0
126
+
127
+ # --- Frequency anomaly via 2D FFT ---
128
+ fft = np.fft.fft2(gray_norm)
129
+ fft_shift = np.fft.fftshift(fft)
130
+ magnitude = np.log1p(np.abs(fft_shift))
131
+ # High-pass: keep frequencies above the center radius (AI images often
132
+ # have unnaturally suppressed high-frequency noise)
133
+ h, w = magnitude.shape
134
+ cy, cx = h // 2, w // 2
135
+ Y, X = np.ogrid[:h, :w]
136
+ r = np.sqrt((X - cx) ** 2 + (Y - cy) ** 2)
137
+ # Anomaly score: deviation of high-freq energy from expected camera noise
138
+ high_freq_mask = r > (min(h, w) * 0.15)
139
+ freq_baseline = magnitude[high_freq_mask].mean()
140
+ freq_map = np.abs(magnitude - freq_baseline)
141
+ freq_map = _normalize(freq_map)
142
+
143
+ # --- Sobel gradient magnitude ---
144
+ ky = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
145
+ kx = ky.T
146
+ gx = _convolve2d(gray_norm, kx)
147
+ gy = _convolve2d(gray_norm, ky)
148
+ grad_map = np.sqrt(gx ** 2 + gy ** 2)
149
+ grad_map = _normalize(grad_map)
150
+
151
+ # Combine: weight by score — high-confidence → emphasize freq anomaly
152
+ alpha = min(confidence_score * 1.2, 0.8)
153
+ combined = alpha * freq_map + (1.0 - alpha) * grad_map
154
+
155
+ # Smooth and normalize
156
+ combined = gaussian_filter(combined, sigma=10)
157
+ return _normalize(combined)
158
+
159
+
160
+ def _convolve2d(img: np.ndarray, kernel: np.ndarray) -> np.ndarray:
161
+ """Manual 2D convolution via stride tricks (no scipy dependency for this)."""
162
+ from scipy.ndimage import convolve
163
+ return convolve(img, kernel, mode="reflect")
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Colormap and overlay helpers
168
+ # ---------------------------------------------------------------------------
169
+
170
+ def _apply_overlay(img_arr: np.ndarray, heat: np.ndarray, alpha: float = 0.55) -> np.ndarray:
171
+ """
172
+ Blend red/yellow heatmap over original image.
173
+ Returns RGBA uint8 array (224, 224, 4).
174
+ """
175
+ # Map heat [0,1] to RGBA: 0=transparent, 0.5=orange, 1.0=bright red
176
+ r = np.ones_like(heat) # R channel: always full
177
+ g = np.clip(1.0 - heat * 1.4, 0, 1) # G: fades out → red
178
+ b = np.zeros_like(heat) # B: always 0
179
+
180
+ overlay_rgb = np.stack([r, g, b], axis=-1) # (224,224,3) float [0,1]
181
+ overlay_alpha = np.clip(heat * alpha * 255, 0, 255) # (224,224) float
182
+
183
+ # Blend: result = img * (1 - a) + color * a
184
+ a3 = (overlay_alpha[:, :, np.newaxis] / 255.0)
185
+ blended = (img_arr / 255.0) * (1.0 - a3) + overlay_rgb * a3
186
+ blended = np.clip(blended * 255, 0, 255).astype(np.uint8)
187
+
188
+ # Add alpha channel
189
+ alpha_ch = overlay_alpha.astype(np.uint8)
190
+ # Keep full opacity everywhere, just use blend for color
191
+ full_alpha = np.full((224, 224), 255, dtype=np.uint8)
192
+ rgba = np.dstack([blended, full_alpha])
193
+ return rgba
194
+
195
+
196
+ def _encode_png(rgba_arr: np.ndarray) -> str:
197
+ """Encode RGBA array to data URI."""
198
+ pil_img = Image.fromarray(rgba_arr, mode="RGBA")
199
+ buf = io.BytesIO()
200
+ pil_img.save(buf, format="PNG", optimize=True)
201
+ b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
202
+ return f"data:image/png;base64,{b64}"
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Utility helpers
207
+ # ---------------------------------------------------------------------------
208
+
209
+ def _normalize(arr: np.ndarray) -> np.ndarray:
210
+ mn, mx = arr.min(), arr.max()
211
+ if mx - mn < 1e-8:
212
+ return np.zeros_like(arr, dtype=np.float32)
213
+ return ((arr - mn) / (mx - mn)).astype(np.float32)
214
+
215
+
216
+ def _upsample(patch_map: np.ndarray, target_h: int, target_w: int) -> np.ndarray:
217
+ """Bilinear upsample a small 2D patch map to target size using PIL."""
218
+ pil = Image.fromarray((patch_map * 255).astype(np.uint8), mode="L")
219
+ pil = pil.resize((target_w, target_h), Image.BILINEAR)
220
+ return np.array(pil, dtype=np.float32) / 255.0
inference.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepGuard — ONNX ViT Inference Module
3
+ Loads the deepfake detection model once at startup.
4
+ All inference is stateless and in-memory.
5
+
6
+ Model: onnx-community/Deep-Fake-Detector-v2-Model-ONNX
7
+ - Architecture: google/vit-base-patch16-224
8
+ - Labels: {0: "Realism", 1: "Deepfake"}
9
+ - Input: pixel_values (1, 3, 224, 224) float32
10
+ - Output: logits (1, 2) float32
11
+ """
12
+
13
+ import os
14
+ import io
15
+ import numpy as np
16
+ from PIL import Image
17
+ from typing import Optional, Tuple
18
+ import onnxruntime as ort
19
+
20
+ # ImageNet normalization constants (used during ViT pre-training)
21
+ IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
22
+ IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
23
+
24
+ MODEL_PATH = os.path.join(os.path.dirname(__file__), "models", "deepfake_vit.onnx")
25
+
26
+ # Module-level singleton — loaded once, reused for every request
27
+ _session: Optional[ort.InferenceSession] = None
28
+ _input_name: str = ""
29
+ _output_names: list[str] = []
30
+ _has_attention_outputs: bool = False
31
+
32
+
33
+ def load_model() -> None:
34
+ """
35
+ Load the ONNX model into a global session at startup.
36
+ Must be called once before any inference.
37
+ """
38
+ global _session, _input_name, _output_names, _has_attention_outputs
39
+
40
+ if not os.path.exists(MODEL_PATH):
41
+ raise FileNotFoundError(
42
+ f"Model not found at {MODEL_PATH}. "
43
+ "Please run: python download_model.py"
44
+ )
45
+
46
+ opts = ort.SessionOptions()
47
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
48
+ opts.inter_op_num_threads = 4
49
+ opts.intra_op_num_threads = 4
50
+
51
+ _session = ort.InferenceSession(
52
+ MODEL_PATH,
53
+ sess_options=opts,
54
+ providers=["CPUExecutionProvider"],
55
+ )
56
+
57
+ _input_name = _session.get_inputs()[0].name
58
+ _output_names = [o.name for o in _session.get_outputs()]
59
+
60
+ # Check whether model exposes attention weights (for attention rollout heatmap)
61
+ _has_attention_outputs = any(
62
+ "attn" in n.lower() or "attention" in n.lower()
63
+ for n in _output_names
64
+ )
65
+
66
+ print(f"[DeepGuard] Model loaded: {MODEL_PATH}")
67
+ print(f"[DeepGuard] Input: {_input_name}")
68
+ print(f"[DeepGuard] Outputs: {_output_names}")
69
+ print(f"[DeepGuard] Attention outputs available: {_has_attention_outputs}")
70
+
71
+
72
+ def get_session() -> ort.InferenceSession:
73
+ if _session is None:
74
+ raise RuntimeError("Model not loaded. Call load_model() first.")
75
+ return _session
76
+
77
+
78
+ def has_attention_outputs() -> bool:
79
+ return _has_attention_outputs
80
+
81
+
82
+ def get_attention_output_names() -> list[str]:
83
+ return [n for n in _output_names if "attn" in n.lower() or "attention" in n.lower()]
84
+
85
+
86
+ def preprocess(image: Image.Image) -> np.ndarray:
87
+ """
88
+ Preprocess a PIL Image for ViT inference.
89
+ Returns: float32 NCHW tensor of shape (1, 3, 224, 224)
90
+ """
91
+ img = image.convert("RGB").resize((224, 224), Image.BILINEAR)
92
+ arr = np.array(img, dtype=np.float32) / 255.0 # (224, 224, 3) [0, 1]
93
+ arr = (arr - IMAGENET_MEAN) / IMAGENET_STD # Normalize
94
+ arr = arr.transpose(2, 0, 1) # HWC → CHW
95
+ arr = np.expand_dims(arr, axis=0) # CHW → NCHW (1,3,224,224)
96
+ return arr
97
+
98
+
99
+ def softmax(logits: np.ndarray) -> np.ndarray:
100
+ """Numerically stable softmax."""
101
+ e = np.exp(logits - np.max(logits))
102
+ return e / e.sum()
103
+
104
+
105
+ def run_inference(image: Image.Image) -> Tuple[float, dict]:
106
+ """
107
+ Run the deepfake detection model on a PIL image.
108
+
109
+ Returns:
110
+ confidence_score (float): Probability of being AI-generated [0.0, 1.0]
111
+ raw_outputs (dict): Full ONNX output dict (for heatmap module)
112
+ """
113
+ session = get_session()
114
+ tensor = preprocess(image)
115
+
116
+ # Run with all outputs (logits + any attention matrices)
117
+ raw_outputs = session.run(None, {_input_name: tensor})
118
+ output_dict = dict(zip(_output_names, raw_outputs))
119
+
120
+ # Find logits output (first non-attention output, or output named 'logits')
121
+ logits_key = next(
122
+ (n for n in _output_names if "logit" in n.lower()),
123
+ _output_names[0]
124
+ )
125
+ logits = output_dict[logits_key].squeeze() # shape (2,)
126
+
127
+ probs = softmax(logits)
128
+ # Label mapping: {0: "Realism", 1: "Deepfake"}
129
+ confidence_score = float(probs[1]) # probability of being Deepfake
130
+
131
+ return confidence_score, output_dict
132
+
133
+
134
+ def get_threat_level(score: float) -> str:
135
+ """Map confidence score to threat level label."""
136
+ if score >= 0.90:
137
+ return "CRITICAL"
138
+ elif score >= 0.75:
139
+ return "HIGH"
140
+ elif score >= 0.50:
141
+ return "MEDIUM"
142
+ else:
143
+ return "LOW"
144
+
145
+
146
+ def get_model_reasoning(score: float, has_exif: bool, software: str) -> str:
147
+ """Generate a human-readable model reasoning string."""
148
+ reasons = []
149
+
150
+ if score >= 0.90:
151
+ reasons.append("Very high-confidence AI artifact signatures detected across multiple image regions.")
152
+ elif score >= 0.75:
153
+ reasons.append("Significant statistical anomalies inconsistent with optical camera sensors detected.")
154
+ elif score >= 0.50:
155
+ reasons.append("Moderate AI artifact patterns detected; image may be partially manipulated.")
156
+ else:
157
+ reasons.append("Low probability of AI generation; image statistics consistent with real photography.")
158
+
159
+ if not has_exif:
160
+ reasons.append("Absence of EXIF metadata is a strong AI indicator.")
161
+ if software != "None":
162
+ reasons.append(f"Known AI software tag '{software}' detected in image metadata.")
163
+
164
+ reasons.append(
165
+ "ViT attention model flagged inconsistencies in background frequency, "
166
+ "texture uniformity, and facial boundary regions."
167
+ )
168
+
169
+ return " ".join(reasons)
main.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepGuard — FastAPI Backend
3
+ Real-time, stateless deepfake detection API.
4
+
5
+ Endpoints:
6
+ GET /health — Liveness check (used by extension popup)
7
+ POST /analyze — Analyze an image for AI-generation artifacts
8
+
9
+ All data is processed in RAM and dropped immediately after the response.
10
+ """
11
+
12
+ import io
13
+ import base64
14
+ import traceback
15
+ from contextlib import asynccontextmanager
16
+
17
+ from fastapi import FastAPI, HTTPException
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from pydantic import BaseModel
20
+ from PIL import Image
21
+
22
+ import inference
23
+ import metadata as meta_module
24
+ import heatmap as heatmap_module
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Lifespan: load model once at startup
29
+ # ---------------------------------------------------------------------------
30
+
31
+ @asynccontextmanager
32
+ async def lifespan(app: FastAPI):
33
+ print("[DeepGuard] Starting up — loading ONNX model...")
34
+ try:
35
+ inference.load_model()
36
+ print("[DeepGuard] Model ready. Server is live at http://localhost:8000")
37
+ except FileNotFoundError as e:
38
+ print(f"[DeepGuard] WARNING: {e}")
39
+ print("[DeepGuard] Run 'python download_model.py' to fetch the model.")
40
+ yield
41
+ print("[DeepGuard] Shutting down.")
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # App setup
46
+ # ---------------------------------------------------------------------------
47
+
48
+ app = FastAPI(
49
+ title="DeepGuard API",
50
+ description="Real-time stateless deepfake detection using ViT + ONNX Runtime",
51
+ version="1.0.0",
52
+ lifespan=lifespan,
53
+ )
54
+
55
+ # Aggressive CORS — required because Chrome extensions use a chrome-extension:// origin
56
+ app.add_middleware(
57
+ CORSMiddleware,
58
+ allow_origins=["*"],
59
+ allow_credentials=False,
60
+ allow_methods=["*"],
61
+ allow_headers=["*"],
62
+ )
63
+
64
+
65
+ import ela as ela_module
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Request / Response schemas
70
+ # ---------------------------------------------------------------------------
71
+
72
+ class AnalyzeRequest(BaseModel):
73
+ image_data: str # data:image/...;base64,<payload> OR raw base64
74
+
75
+
76
+ class MetadataResult(BaseModel):
77
+ exif_data_present: bool
78
+ software_signature_found: str
79
+ warning: str
80
+
81
+
82
+ class ForensicsResult(BaseModel):
83
+ model_reasoning: str
84
+ metadata: MetadataResult
85
+
86
+
87
+ class AnalyzeResponse(BaseModel):
88
+ status: str
89
+ threat_level: str
90
+ confidence_score: float
91
+ heatmap_overlay_url: str
92
+ ela_overlay_url: str
93
+ forensics: ForensicsResult
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Helpers
98
+ # ---------------------------------------------------------------------------
99
+
100
+ def _decode_image(image_data: str) -> tuple[bytes, Image.Image]:
101
+ """
102
+ Decode a base64 data-URI or raw base64 string into (raw_bytes, PIL Image).
103
+ """
104
+ if image_data.startswith("data:"):
105
+ # Strip "data:image/jpeg;base64," prefix
106
+ header, b64_str = image_data.split(",", 1)
107
+ else:
108
+ b64_str = image_data
109
+
110
+ try:
111
+ raw_bytes = base64.b64decode(b64_str)
112
+ except Exception:
113
+ raise HTTPException(status_code=400, detail="Invalid base64 image data.")
114
+
115
+ try:
116
+ image = Image.open(io.BytesIO(raw_bytes)).convert("RGB")
117
+ except Exception:
118
+ raise HTTPException(status_code=400, detail="Could not decode image from base64 payload.")
119
+
120
+ return raw_bytes, image
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Endpoints
125
+ # ---------------------------------------------------------------------------
126
+
127
+ @app.get("/health")
128
+ async def health():
129
+ """Liveness check. Returns model load status."""
130
+ try:
131
+ session = inference.get_session()
132
+ return {
133
+ "status": "ok",
134
+ "model_loaded": True,
135
+ "attention_heatmap": inference.has_attention_outputs(),
136
+ }
137
+ except RuntimeError:
138
+ return {
139
+ "status": "degraded",
140
+ "model_loaded": False,
141
+ "attention_heatmap": False,
142
+ "message": "Model not loaded. Run python download_model.py",
143
+ }
144
+
145
+
146
+ @app.post("/analyze", response_model=AnalyzeResponse)
147
+ async def analyze(request: AnalyzeRequest):
148
+ """
149
+ Main analysis endpoint.
150
+ Accepts a base64-encoded image and returns:
151
+ - Deepfake confidence score
152
+ - Threat level classification
153
+ - Grad-CAM / attention heatmap overlay (base64 PNG)
154
+ - EXIF metadata forensics
155
+ """
156
+ # ── 1. Decode image ──────────────────────────────────────────────────
157
+ try:
158
+ raw_bytes, image = _decode_image(request.image_data)
159
+ except HTTPException:
160
+ raise
161
+ except Exception as e:
162
+ raise HTTPException(status_code=400, detail=f"Image decode error: {e}")
163
+
164
+ # ── 2. Metadata forensics (DFIR) ���────────────────────────────────────
165
+ try:
166
+ forensic_meta = meta_module.extract_metadata(raw_bytes)
167
+ except Exception:
168
+ forensic_meta = {
169
+ "exif_data_present": False,
170
+ "software_signature_found": "None",
171
+ "warning": "Metadata extraction failed.",
172
+ }
173
+
174
+ # ── 3. AI inference ───────────────────────────────────────────────────
175
+ try:
176
+ confidence_score, output_dict = inference.run_inference(image)
177
+ except RuntimeError as e:
178
+ raise HTTPException(
179
+ status_code=503,
180
+ detail=f"Model not loaded: {e}. Run python download_model.py first.",
181
+ )
182
+ except Exception as e:
183
+ import traceback
184
+ traceback.print_exc()
185
+ raise HTTPException(status_code=500, detail=f"Inference error: {e}")
186
+
187
+ # ── 4. Heatmap & ELA generation ────────────────────────────────────────
188
+ try:
189
+ heatmap_url = heatmap_module.generate_heatmap(image, output_dict, confidence_score)
190
+ except Exception:
191
+ import traceback
192
+ traceback.print_exc()
193
+ # Fallback: return a 1×1 transparent PNG
194
+ heatmap_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
195
+
196
+ # Run Error Level Analysis (ELA)
197
+ ela_url = ela_module.generate_ela(image)
198
+
199
+ # ── 5. Build response ─────────────────────────────────────────────────
200
+ threat_level = inference.get_threat_level(confidence_score)
201
+ model_reasoning = inference.get_model_reasoning(
202
+ confidence_score,
203
+ forensic_meta["exif_data_present"],
204
+ forensic_meta["software_signature_found"],
205
+ )
206
+
207
+ return AnalyzeResponse(
208
+ status="success",
209
+ threat_level=threat_level,
210
+ confidence_score=round(confidence_score, 4),
211
+ heatmap_overlay_url=heatmap_url,
212
+ ela_overlay_url=ela_url,
213
+ forensics=ForensicsResult(
214
+ model_reasoning=model_reasoning,
215
+ metadata=MetadataResult(**forensic_meta),
216
+ ),
217
+ )
metadata.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepGuard — Metadata Forensics Module
3
+ Extracts EXIF data and checks for known AI software signatures.
4
+ All processing is stateless and in-memory.
5
+ """
6
+
7
+ import io
8
+ import struct
9
+ from typing import Optional
10
+ from PIL import Image
11
+
12
+ # Known AI generation software signatures to look for in EXIF/metadata
13
+ AI_SIGNATURES = [
14
+ "DALL-E", "dall-e", "Midjourney", "midjourney",
15
+ "Stable Diffusion", "stable-diffusion", "StableDiffusion",
16
+ "Adobe Firefly", "firefly", "Sora", "sora",
17
+ "Imagen", "imagen", "Bing Image Creator",
18
+ "NightCafe", "Craiyon", "FLUX", "flux",
19
+ "Runway", "runway", "Pika", "pika",
20
+ "ComfyUI", "comfyui", "Automatic1111", "InvokeAI",
21
+ "NovelAI", "novelai", "Leonardo", "leonardo.ai",
22
+ ]
23
+
24
+
25
+ def extract_metadata(image_bytes: bytes) -> dict:
26
+ """
27
+ Perform forensic metadata analysis on raw image bytes.
28
+
29
+ Returns:
30
+ dict with keys: exif_data_present, software_signature_found, warning
31
+ """
32
+ exif_present = False
33
+ software_found = "None"
34
+ warning = ""
35
+
36
+ try:
37
+ img = Image.open(io.BytesIO(image_bytes))
38
+ fmt = img.format or "UNKNOWN"
39
+
40
+ # --- EXIF analysis (JPEG / TIFF / WEBP) ---
41
+ exif_data = img._getexif() if hasattr(img, "_getexif") else None
42
+
43
+ if exif_data:
44
+ exif_present = True
45
+ # Tag 305 = Software, Tag 315 = Artist, Tag 270 = ImageDescription
46
+ tag_values = {
47
+ 305: "Software",
48
+ 315: "Artist",
49
+ 270: "ImageDescription",
50
+ 37510: "UserComment",
51
+ }
52
+ for tag_id, tag_name in tag_values.items():
53
+ val = exif_data.get(tag_id, "")
54
+ if isinstance(val, bytes):
55
+ try:
56
+ val = val.decode("utf-8", errors="ignore")
57
+ except Exception:
58
+ val = ""
59
+ val_str = str(val)
60
+ for sig in AI_SIGNATURES:
61
+ if sig.lower() in val_str.lower():
62
+ software_found = sig
63
+ break
64
+ if software_found != "None":
65
+ break
66
+ else:
67
+ # Try PIL's generic info dict (PNG tEXt chunks, etc.)
68
+ info = getattr(img, "info", {})
69
+ if info:
70
+ exif_present = True # Has some metadata
71
+ info_str = " ".join(str(v) for v in info.values())
72
+ for sig in AI_SIGNATURES:
73
+ if sig.lower() in info_str.lower():
74
+ software_found = sig
75
+ break
76
+ else:
77
+ exif_present = False
78
+
79
+ # Build warning message
80
+ if not exif_present:
81
+ warning = (
82
+ "EXIF data missing. This is a strong indicator of synthesized media — "
83
+ "AI generators strip or never write camera metadata."
84
+ )
85
+ elif software_found != "None":
86
+ warning = (
87
+ f"AI software signature detected: '{software_found}'. "
88
+ "This image was almost certainly generated by an AI tool."
89
+ )
90
+ else:
91
+ warning = (
92
+ "EXIF data present. Metadata appears consistent with a camera-captured image, "
93
+ "but AI-generated images can be post-processed to include fake EXIF."
94
+ )
95
+
96
+ except Exception as e:
97
+ warning = f"Metadata parsing error: {str(e)}"
98
+
99
+ return {
100
+ "exif_data_present": exif_present,
101
+ "software_signature_found": software_found,
102
+ "warning": warning,
103
+ }
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.6
3
+ onnxruntime==1.19.2
4
+ Pillow==10.4.0
5
+ numpy==1.26.4
6
+ piexif==1.1.3
7
+ scipy==1.14.1
8
+ python-multipart==0.0.9
9
+ huggingface_hub==0.24.6
10
+ requests==2.32.3