AdeelHassan commited on
Commit
c317afe
Β·
verified Β·
1 Parent(s): acd82d1

final touch

Browse files
Files changed (4) hide show
  1. Dockerfile +1 -1
  2. app.py +8 -5
  3. inference.py +115 -92
  4. requirements.txt +3 -3
Dockerfile CHANGED
@@ -6,4 +6,4 @@ RUN pip install --no-cache-dir torch torchvision --index-url https://download.py
6
  RUN pip install --no-cache-dir -r requirements.txt
7
  COPY . .
8
  EXPOSE 7860
9
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
6
  RUN pip install --no-cache-dir -r requirements.txt
7
  COPY . .
8
  EXPOSE 7860
9
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
app.py CHANGED
@@ -17,8 +17,8 @@ logger = logging.getLogger("deep-detect-api")
17
  # Initialize FastAPI App
18
  app = FastAPI(
19
  title="Deep-Detect API",
20
- description="Production-grade API for AI vs Real Image Detection using a custom CNN.",
21
- version="1.0.0"
22
  )
23
 
24
  # Enable CORS middleware
@@ -56,13 +56,16 @@ async def predict_image(file: UploadFile = File(...)):
56
  """
57
  logger.info(f"Received prediction request. File: {file.filename}")
58
 
59
- # Validate file extension
60
  content_type = file.content_type or ""
61
- if not (content_type.startswith("image/") or file.filename.lower().endswith((".png", ".jpg", ".jpeg"))):
 
 
 
62
  logger.warning(f"Rejected invalid file format: {file.filename} (Content-Type: {content_type})")
63
  raise HTTPException(
64
  status_code=status.HTTP_400_BAD_REQUEST,
65
- detail="Uploaded file must be a valid JPEG or PNG image."
66
  )
67
 
68
  try:
 
17
  # Initialize FastAPI App
18
  app = FastAPI(
19
  title="Deep-Detect API",
20
+ description="Production-grade API for AI-generated vs Real image detection. Uses a 3-model ensemble (Custom CNN + ViT Deepfake Detection + Deepfake vs Real ViT) with majority voting for robust predictions.",
21
+ version="2.0.0"
22
  )
23
 
24
  # Enable CORS middleware
 
56
  """
57
  logger.info(f"Received prediction request. File: {file.filename}")
58
 
59
+ # Validate file is a supported image format
60
  content_type = file.content_type or ""
61
+ allowed_extensions = (".png", ".jpg", ".jpeg", ".webp")
62
+ is_valid_type = content_type.startswith("image/")
63
+ is_valid_ext = file.filename.lower().endswith(allowed_extensions)
64
+ if not (is_valid_type or is_valid_ext):
65
  logger.warning(f"Rejected invalid file format: {file.filename} (Content-Type: {content_type})")
66
  raise HTTPException(
67
  status_code=status.HTTP_400_BAD_REQUEST,
68
+ detail="Uploaded file must be a valid image (JPEG, PNG, or WebP)."
69
  )
70
 
71
  try:
inference.py CHANGED
@@ -2,6 +2,7 @@ import io
2
  import os
3
  import logging
4
  import warnings
 
5
 
6
  # ─── Silence ALL third-party noise before any imports ──────────────────────────
7
  os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
@@ -19,33 +20,30 @@ import torch
19
  from PIL import Image
20
  from torchvision import transforms
21
 
22
-
23
- # Set up logging for custom inference pipeline
24
  logging.basicConfig(level=logging.INFO)
25
  logger = logging.getLogger("inference")
26
 
27
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
28
- # Note: The folder is "models", and model is "custom_cnn_standalone.pt"
29
  MODEL_PATH = os.path.join(BASE_DIR, "models", "custom_cnn_standalone.pt")
30
 
31
- # GPU/CPU Device mapping
32
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
33
  logger.info(f"Using device: {device} for inference.")
34
 
35
  # Hyperparameters matching the Custom CNN Standalone training
36
  IMG_SIZE = 224
37
  MEAN = [0.485, 0.456, 0.406]
38
- STD = [0.229, 0.224, 0.225]
39
  OPTIMAL_THRESHOLD = 0.5
40
 
41
  # Image Preprocessing Transformation Pipeline
42
- transform = transforms.Compose(
43
- [
44
- transforms.Resize((IMG_SIZE, IMG_SIZE)),
45
- transforms.ToTensor(),
46
- transforms.Normalize(mean=MEAN, std=STD),
47
- ]
48
- )
49
 
50
  # Load the compiled TorchScript model
51
  if not os.path.exists(MODEL_PATH):
@@ -65,77 +63,91 @@ except Exception as e:
65
  raise e
66
 
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  def _predict_probability(image: Image.Image) -> float:
69
  """
70
  Pass the preprocessed image through the loaded Custom CNN model.
71
  Applies Sigmoid to the output logit to compute probability.
72
  """
73
  input_tensor = transform(image.convert("RGB")).unsqueeze(0).to(device)
74
-
75
  with torch.no_grad():
76
  output = model(input_tensor)
77
- # Apply sigmoid since model outputs a single class raw logit
78
  probability = torch.sigmoid(output).item()
79
  return probability
80
 
81
 
82
- def _load_image(image_source: str | bytes | Image.Image) -> Image.Image:
83
- """
84
- Load an image from filepath, raw bytes, or an existing PIL Image.
85
- """
86
- if isinstance(image_source, Image.Image):
87
- return image_source
88
- if isinstance(image_source, bytes):
89
- return Image.open(io.BytesIO(image_source))
90
- return Image.open(image_source)
91
-
92
 
93
- # Sentinel values: None = not yet attempted, False = failed, object = loaded
94
  _model_2 = None
95
  _model_3 = None
 
96
 
97
 
98
- def _load_hf_models():
99
  """
100
- Load both Hugging Face models exactly once. Uses a sentinel pattern:
101
- - None β†’ not yet attempted (first request will trigger load)
102
- - False β†’ previously failed (skip silently, no retry)
103
- - obj β†’ successfully loaded pipeline
104
  """
105
  global _model_2, _model_3
106
- try:
107
- from transformers import pipeline
108
-
109
- if _model_2 is None:
110
- try:
111
- # GAN-focused: ViT fine-tuned on GAN-generated face datasets
112
- _model_2 = pipeline(
113
- "image-classification",
114
- model="Wvolf/ViT_Deepfake_Detection",
115
- device=-1,
116
- )
117
- except Exception:
118
- _model_2 = False # Mark as permanently failed, never retry
119
-
120
- if _model_3 is None:
121
- try:
122
- # Diffusion-focused: ViT fine-tuned on Stable Diffusion / deepfake datasets
123
- _model_3 = pipeline(
124
- "image-classification",
125
- model="dima806/deepfake_vs_real_image_detection",
126
- device=-1,
127
- )
128
- except Exception:
129
- _model_3 = False # Mark as permanently failed, never retry
130
-
131
- except Exception:
132
- if _model_2 is None:
133
- _model_2 = False
134
- if _model_3 is None:
135
- _model_3 = False
136
-
137
-
138
- def _predict_hf_model_2(pil_image) -> str:
 
139
  if _model_2 is None:
140
  _load_hf_models()
141
  if not _model_2:
@@ -149,7 +161,7 @@ def _predict_hf_model_2(pil_image) -> str:
149
  return "unknown"
150
 
151
 
152
- def _predict_hf_model_3(pil_image) -> str:
153
  if _model_3 is None:
154
  _load_hf_models()
155
  if not _model_3:
@@ -163,8 +175,9 @@ def _predict_hf_model_3(pil_image) -> str:
163
  return "unknown"
164
 
165
 
 
166
 
167
- def predict_label(image_source: str | bytes | Image.Image) -> str:
168
  """
169
  Predict if the image is 'real' or 'ai' using majority voting.
170
  """
@@ -172,51 +185,61 @@ def predict_label(image_source: str | bytes | Image.Image) -> str:
172
  return label
173
 
174
 
175
- def predict_with_confidence(image_source: str | bytes | Image.Image) -> tuple[str, float]:
176
  """
177
  Predict if the image is 'real' or 'ai' and return the confidence percentage.
178
- Uses an ensemble majority vote (2 out of 3) executed concurrently for minimal latency,
179
- maintaining absolute logging privacy.
 
 
 
 
 
 
180
  """
181
  from concurrent.futures import ThreadPoolExecutor
182
-
183
  image = _load_image(image_source)
184
 
185
- # Execute all 3 model predictions concurrently to achieve minimal latency
186
  with ThreadPoolExecutor(max_workers=3) as executor:
187
  future_custom = executor.submit(_predict_probability, image)
188
- future_hf2 = executor.submit(_predict_hf_model_2, image)
189
- future_hf3 = executor.submit(_predict_hf_model_3, image)
190
 
191
- # Retrieve predictions in parallel
192
  probability = future_custom.result()
193
- m2_label = future_hf2.result()
194
- m3_label = future_hf3.result()
195
 
196
- # Process custom model results
197
- if probability > OPTIMAL_THRESHOLD:
198
- custom_label = "real"
199
- custom_confidence = probability * 100
200
- else:
201
- custom_label = "ai"
202
- custom_confidence = (1.0 - probability) * 100
203
 
204
- # Perform majority voting
205
  votes = [custom_label]
206
  if m2_label != "unknown":
207
  votes.append(m2_label)
208
  if m3_label != "unknown":
209
  votes.append(m3_label)
210
 
211
- ai_votes = votes.count("ai")
212
- real_votes = votes.count("real")
213
- final_label = "ai" if ai_votes > real_votes else "real"
214
 
215
- # Match the output confidence structure of the custom model
216
- if final_label == custom_label:
217
- final_confidence = custom_confidence
 
218
  else:
219
- # Custom model was overridden; return a realistic confidence percentage
220
- final_confidence = max(51.0, 100.0 - custom_confidence)
 
 
 
 
 
 
 
 
 
221
 
222
- return final_label, final_confidence
 
2
  import os
3
  import logging
4
  import warnings
5
+ import threading
6
 
7
  # ─── Silence ALL third-party noise before any imports ──────────────────────────
8
  os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
 
20
  from PIL import Image
21
  from torchvision import transforms
22
 
23
+ # Set up logging for custom inference pipeline only
 
24
  logging.basicConfig(level=logging.INFO)
25
  logger = logging.getLogger("inference")
26
 
27
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
 
28
  MODEL_PATH = os.path.join(BASE_DIR, "models", "custom_cnn_standalone.pt")
29
 
30
+ # GPU/CPU device β€” custom model uses same device as HF models
31
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
32
+ hf_device = 0 if torch.cuda.is_available() else -1 # Fix: use GPU for HF models if available
33
  logger.info(f"Using device: {device} for inference.")
34
 
35
  # Hyperparameters matching the Custom CNN Standalone training
36
  IMG_SIZE = 224
37
  MEAN = [0.485, 0.456, 0.406]
38
+ STD = [0.229, 0.224, 0.225]
39
  OPTIMAL_THRESHOLD = 0.5
40
 
41
  # Image Preprocessing Transformation Pipeline
42
+ transform = transforms.Compose([
43
+ transforms.Resize((IMG_SIZE, IMG_SIZE)),
44
+ transforms.ToTensor(),
45
+ transforms.Normalize(mean=MEAN, std=STD),
46
+ ])
 
 
47
 
48
  # Load the compiled TorchScript model
49
  if not os.path.exists(MODEL_PATH):
 
63
  raise e
64
 
65
 
66
+ # ─── Image Loading (with validation) ──────────────────────────────────────────
67
+
68
+ def _load_image(image_source: "str | bytes | Image.Image") -> Image.Image:
69
+ """
70
+ Load an image from filepath, raw bytes, or an existing PIL Image.
71
+ Raises ValueError with a clear message on any failure.
72
+ """
73
+ try:
74
+ if isinstance(image_source, Image.Image):
75
+ return image_source
76
+ if isinstance(image_source, bytes):
77
+ return Image.open(io.BytesIO(image_source))
78
+ if not os.path.exists(image_source):
79
+ raise FileNotFoundError(f"Image not found: {image_source}")
80
+ return Image.open(image_source)
81
+ except Exception as e:
82
+ raise ValueError(f"Failed to load image: {e}")
83
+
84
+
85
+ # ─── Custom Model Inference ────────────────────────────────────────────────────
86
+
87
  def _predict_probability(image: Image.Image) -> float:
88
  """
89
  Pass the preprocessed image through the loaded Custom CNN model.
90
  Applies Sigmoid to the output logit to compute probability.
91
  """
92
  input_tensor = transform(image.convert("RGB")).unsqueeze(0).to(device)
 
93
  with torch.no_grad():
94
  output = model(input_tensor)
 
95
  probability = torch.sigmoid(output).item()
96
  return probability
97
 
98
 
99
+ # ─── Hugging Face Models β€” thread-safe lazy load with sentinel pattern ─────────
100
+ # Sentinel values:
101
+ # None β†’ not yet attempted (first request will trigger load)
102
+ # False β†’ permanently failed (never retry)
103
+ # obj β†’ loaded pipeline
 
 
 
 
 
104
 
 
105
  _model_2 = None
106
  _model_3 = None
107
+ _hf_lock = threading.Lock() # Fix: thread-safe loading
108
 
109
 
110
+ def _load_hf_models() -> None:
111
  """
112
+ Load both HF models exactly once under a mutex.
113
+ Individual model failures are caught independently so one
114
+ bad model does not prevent the other from loading.
 
115
  """
116
  global _model_2, _model_3
117
+ with _hf_lock:
118
+ try:
119
+ from transformers import pipeline
120
+
121
+ if _model_2 is None:
122
+ try:
123
+ # GAN-focused: ViT fine-tuned on GAN / deepfake face datasets
124
+ _model_2 = pipeline(
125
+ "image-classification",
126
+ model="Wvolf/ViT_Deepfake_Detection",
127
+ device=hf_device,
128
+ )
129
+ except Exception:
130
+ _model_2 = False
131
+
132
+ if _model_3 is None:
133
+ try:
134
+ # Diffusion-focused: ViT fine-tuned on Stable Diffusion datasets
135
+ _model_3 = pipeline(
136
+ "image-classification",
137
+ model="dima806/deepfake_vs_real_image_detection",
138
+ device=hf_device,
139
+ )
140
+ except Exception:
141
+ _model_3 = False
142
+
143
+ except Exception:
144
+ if _model_2 is None:
145
+ _model_2 = False
146
+ if _model_3 is None:
147
+ _model_3 = False
148
+
149
+
150
+ def _predict_hf_model_2(pil_image: Image.Image) -> str:
151
  if _model_2 is None:
152
  _load_hf_models()
153
  if not _model_2:
 
161
  return "unknown"
162
 
163
 
164
+ def _predict_hf_model_3(pil_image: Image.Image) -> str:
165
  if _model_3 is None:
166
  _load_hf_models()
167
  if not _model_3:
 
175
  return "unknown"
176
 
177
 
178
+ # ─── Public API ───────────────────────────────────────────────────────────────
179
 
180
+ def predict_label(image_source: "str | bytes | Image.Image") -> str:
181
  """
182
  Predict if the image is 'real' or 'ai' using majority voting.
183
  """
 
185
  return label
186
 
187
 
188
+ def predict_with_confidence(image_source: "str | bytes | Image.Image") -> tuple:
189
  """
190
  Predict if the image is 'real' or 'ai' and return the confidence percentage.
191
+
192
+ Execution strategy:
193
+ - All 3 models run concurrently via ThreadPoolExecutor for minimum latency.
194
+ - Majority vote determines the final label.
195
+ - Confidence is computed as (winning_votes / total_votes) * 100 β€” honest
196
+ and meaningful regardless of how many models are available.
197
+ - If only 1 model voted (both HF models failed), confidence is capped at
198
+ 60% to reflect reduced certainty from the lack of ensemble.
199
  """
200
  from concurrent.futures import ThreadPoolExecutor
201
+
202
  image = _load_image(image_source)
203
 
204
+ # Run all 3 models concurrently
205
  with ThreadPoolExecutor(max_workers=3) as executor:
206
  future_custom = executor.submit(_predict_probability, image)
207
+ future_hf2 = executor.submit(_predict_hf_model_2, image)
208
+ future_hf3 = executor.submit(_predict_hf_model_3, image)
209
 
 
210
  probability = future_custom.result()
211
+ m2_label = future_hf2.result()
212
+ m3_label = future_hf3.result()
213
 
214
+ # Resolve custom model label
215
+ custom_label = "real" if probability > OPTIMAL_THRESHOLD else "ai"
 
 
 
 
 
216
 
217
+ # Build vote list β€” only include models that returned a valid prediction
218
  votes = [custom_label]
219
  if m2_label != "unknown":
220
  votes.append(m2_label)
221
  if m3_label != "unknown":
222
  votes.append(m3_label)
223
 
224
+ ai_votes = votes.count("ai")
225
+ real_votes = votes.count("real")
226
+ total_votes = len(votes)
227
 
228
+ if ai_votes > real_votes:
229
+ final_label = "ai"
230
+ elif real_votes > ai_votes:
231
+ final_label = "real"
232
  else:
233
+ # Tie: trust the custom model as the primary authority
234
+ final_label = custom_label
235
+
236
+ # Vote-ratio confidence β€” honest and meaningful
237
+ winning_votes = max(ai_votes, real_votes)
238
+ vote_confidence = (winning_votes / total_votes) * 100
239
+
240
+ # If only 1 model voted (both HF models failed), cap confidence to signal
241
+ # reduced certainty β€” the result is not an ensemble, just a single model.
242
+ if total_votes == 1:
243
+ vote_confidence = min(vote_confidence, 60.0)
244
 
245
+ return final_label, round(vote_confidence, 2)
requirements.txt CHANGED
@@ -4,6 +4,6 @@ python-multipart>=0.0.6
4
  torch>=2.0.0
5
  torchvision>=0.15.0
6
  pillow>=9.5.0
7
- transformers>=4.30.0
8
- huggingface_hub>=0.15.0
9
-
 
4
  torch>=2.0.0
5
  torchvision>=0.15.0
6
  pillow>=9.5.0
7
+ transformers>=4.36.0
8
+ huggingface_hub>=0.20.0
9
+ accelerate>=0.25.0