AdeelHassan commited on
Commit
c5addcf
·
verified ·
1 Parent(s): 9b680ce
Files changed (4) hide show
  1. Dockerfile +1 -1
  2. app.py +5 -8
  3. inference.py +35 -180
  4. requirements.txt +0 -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", "--workers", "1"]
 
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"]
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-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,16 +56,13 @@ async def predict_image(file: UploadFile = File(...)):
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:
 
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
  """
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:
inference.py CHANGED
@@ -1,49 +1,36 @@
1
  import io
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"
9
- os.environ["TRANSFORMERS_VERBOSITY"] = "error"
10
- os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
11
- os.environ["TOKENIZERS_PARALLELISM"] = "false"
12
-
13
- warnings.filterwarnings("ignore")
14
-
15
- for _noisy in ("transformers", "huggingface_hub", "huggingface_hub.utils._http",
16
- "urllib3", "httpx", "httpcore", "filelock", "fsspec"):
17
- logging.getLogger(_noisy).setLevel(logging.CRITICAL)
18
-
19
  import torch
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,183 +50,51 @@ except Exception as e:
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
- # Exp-02-21: balanced GAN + diffusion deepfake detector
124
- _model_2 = pipeline(
125
- "image-classification",
126
- model="prithivMLmods/Deepfake-Detection-Exp-02-21",
127
- device=hf_device,
128
- )
129
- except Exception:
130
- _model_2 = False
131
-
132
- if _model_3 is None:
133
- try:
134
- # Deep-Fake-Detector-v2: diffusion-focused model (labels inverted)
135
- _model_3 = pipeline(
136
- "image-classification",
137
- model="prithivMLmods/Deep-Fake-Detector-v2-Model",
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:
154
- return "unknown"
155
- try:
156
- results = _model_2(pil_image)
157
- best = max(results, key=lambda x: x["score"])
158
- # Exp-02-21 labels: "Deepfake" / "Real" (correct mapping)
159
- return "ai" if best["label"].lower() == "deepfake" else "real"
160
- except Exception:
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:
168
- return "unknown"
169
- try:
170
- results = _model_3(pil_image)
171
- best = max(results, key=lambda x: x["score"])
172
- # Deep-Fake-Detector-v2 labels are inverted: "Realism" means Deepfake, "Deepfake" means Real
173
- return "ai" if best["label"].lower() == "realism" else "real"
174
- except Exception:
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
  """
184
- label, _ = predict_with_confidence(image_source)
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)
 
1
  import io
2
  import os
3
  import logging
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import torch
5
  from PIL import Image
6
  from torchvision import transforms
7
 
8
+ # Set up logging
9
  logging.basicConfig(level=logging.INFO)
10
  logger = logging.getLogger("inference")
11
 
12
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
13
+ # Note: The folder is "models", and model is "custom_cnn_standalone.pt"
14
  MODEL_PATH = os.path.join(BASE_DIR, "models", "custom_cnn_standalone.pt")
15
 
16
+ # GPU/CPU Device mapping
17
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
18
  logger.info(f"Using device: {device} for inference.")
19
 
20
  # Hyperparameters matching the Custom CNN Standalone training
21
  IMG_SIZE = 224
22
  MEAN = [0.485, 0.456, 0.406]
23
+ STD = [0.229, 0.224, 0.225]
24
  OPTIMAL_THRESHOLD = 0.5
25
 
26
  # Image Preprocessing Transformation Pipeline
27
+ transform = transforms.Compose(
28
+ [
29
+ transforms.Resize((IMG_SIZE, IMG_SIZE)),
30
+ transforms.ToTensor(),
31
+ transforms.Normalize(mean=MEAN, std=STD),
32
+ ]
33
+ )
34
 
35
  # Load the compiled TorchScript model
36
  if not os.path.exists(MODEL_PATH):
 
50
  raise e
51
 
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def _predict_probability(image: Image.Image) -> float:
54
  """
55
  Pass the preprocessed image through the loaded Custom CNN model.
56
  Applies Sigmoid to the output logit to compute probability.
57
  """
58
  input_tensor = transform(image.convert("RGB")).unsqueeze(0).to(device)
59
+
60
  with torch.no_grad():
61
  output = model(input_tensor)
62
+ # Apply sigmoid since model outputs a single class raw logit
63
  probability = torch.sigmoid(output).item()
64
  return probability
65
 
66
 
67
+ def _load_image(image_source: str | bytes | Image.Image) -> Image.Image:
 
 
 
 
 
 
 
 
 
 
 
68
  """
69
+ Load an image from filepath, raw bytes, or an existing PIL Image.
 
 
70
  """
71
+ if isinstance(image_source, Image.Image):
72
+ return image_source
73
+ if isinstance(image_source, bytes):
74
+ return Image.open(io.BytesIO(image_source))
75
+ return Image.open(image_source)
76
+
77
+
78
+ def predict_label(image_source: str | bytes | Image.Image) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  """
80
+ Predict if the image is 'real' or 'ai'.
81
  """
82
+ image = _load_image(image_source)
83
+ probability = _predict_probability(image)
84
+ return "real" if probability > OPTIMAL_THRESHOLD else "ai"
85
 
86
 
87
+ def predict_with_confidence(image_source: str | bytes | Image.Image) -> tuple[str, float]:
88
  """
89
  Predict if the image is 'real' or 'ai' and return the confidence percentage.
90
+
91
+ If probability > 0.5: Class 1 (real). Confidence is probability * 100
92
+ If probability <= 0.5: Class 0 (ai). Confidence is (1.0 - probability) * 100
 
 
 
 
 
93
  """
 
 
94
  image = _load_image(image_source)
95
+ probability = _predict_probability(image)
96
 
97
+ if probability > OPTIMAL_THRESHOLD:
98
+ return "real", probability * 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ return "ai", (1.0 - probability) * 100
requirements.txt CHANGED
@@ -4,6 +4,3 @@ python-multipart>=0.0.6
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
 
4
  torch>=2.0.0
5
  torchvision>=0.15.0
6
  pillow>=9.5.0