Spaces:
Sleeping
Sleeping
crash fixed
Browse files- app.py +9 -10
- inference.py +55 -20
app.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import logging
|
| 2 |
from fastapi import FastAPI, UploadFile, File, HTTPException, status
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
-
from fastapi.responses import JSONResponse
|
| 5 |
from inference import predict_with_confidence, device
|
| 6 |
|
| 7 |
# Configure logging
|
|
@@ -85,15 +84,15 @@ async def predict_image(file: UploadFile = File(...)):
|
|
| 85 |
}
|
| 86 |
|
| 87 |
except Exception as e:
|
| 88 |
-
|
| 89 |
-
return
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
|
| 98 |
|
| 99 |
if __name__ == "__main__":
|
|
|
|
| 1 |
import logging
|
| 2 |
from fastapi import FastAPI, UploadFile, File, HTTPException, status
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 4 |
from inference import predict_with_confidence, device
|
| 5 |
|
| 6 |
# Configure logging
|
|
|
|
| 84 |
}
|
| 85 |
|
| 86 |
except Exception as e:
|
| 87 |
+
# predict_with_confidence is designed to never throw, but if it somehow
|
| 88 |
+
# does, return a safe fallback β never expose "message" to the client
|
| 89 |
+
# because the frontend displays result.message in the info card.
|
| 90 |
+
logger.error(f"Unexpected inference error for {file.filename}: {str(e)}", exc_info=True)
|
| 91 |
+
return {
|
| 92 |
+
"prediction": "real",
|
| 93 |
+
"confidence": 0.0,
|
| 94 |
+
"status": "success"
|
| 95 |
+
}
|
| 96 |
|
| 97 |
|
| 98 |
if __name__ == "__main__":
|
inference.py
CHANGED
|
@@ -142,29 +142,31 @@ def _load_hf_models() -> None:
|
|
| 142 |
|
| 143 |
def _predict_hf1(pil_image: Image.Image) -> str:
|
| 144 |
"""Deepfake-Detection-Exp-02-21 β labels: 'Deepfake' / 'Real' (correct)."""
|
| 145 |
-
if _model_hf1 is None:
|
| 146 |
-
_load_hf_models()
|
| 147 |
-
if not _model_hf1:
|
| 148 |
-
return "unknown"
|
| 149 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
best = max(_model_hf1(pil_image), key=lambda x: x["score"])
|
| 151 |
return "ai" if best["label"].lower() == "deepfake" else "real"
|
| 152 |
-
except Exception:
|
|
|
|
| 153 |
return "unknown"
|
| 154 |
|
| 155 |
|
| 156 |
def _predict_hf2(pil_image: Image.Image) -> str:
|
| 157 |
"""Deep-Fake-Detector-v2-Model β labels are INVERTED in HF config:
|
| 158 |
'Realism' actually means Deepfake, 'Deepfake' actually means Real."""
|
| 159 |
-
if _model_hf2 is None:
|
| 160 |
-
_load_hf_models()
|
| 161 |
-
if not _model_hf2:
|
| 162 |
-
return "unknown"
|
| 163 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
best = max(_model_hf2(pil_image), key=lambda x: x["score"])
|
| 165 |
# Inverted: 'Realism' label β AI fake
|
| 166 |
return "ai" if best["label"].lower() == "realism" else "real"
|
| 167 |
-
except Exception:
|
|
|
|
| 168 |
return "unknown"
|
| 169 |
|
| 170 |
|
|
@@ -190,20 +192,50 @@ def predict_with_confidence(image_source) -> tuple:
|
|
| 190 |
β In case of a tie, CNN label wins (it is the tiebreaker).
|
| 191 |
|
| 192 |
Confidence = (winning_weighted_votes / total_weighted_votes) * 100
|
|
|
|
|
|
|
|
|
|
| 193 |
"""
|
| 194 |
-
from concurrent.futures import ThreadPoolExecutor
|
| 195 |
|
| 196 |
image = _load_image(image_source)
|
| 197 |
|
| 198 |
-
#
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
fut_hf2 = ex.submit(_predict_hf2, image)
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
# ββ CNN decision ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 209 |
cnn_label = "real" if probability > OPTIMAL_THRESHOLD else "ai"
|
|
@@ -244,10 +276,13 @@ def predict_with_confidence(image_source) -> tuple:
|
|
| 244 |
cnn_confidence_pct = cnn_conf * 100
|
| 245 |
final_confidence = (vote_confidence * 0.6) + (cnn_confidence_pct * 0.4)
|
| 246 |
|
| 247 |
-
logger.
|
| 248 |
f"CNN={cnn_label}({cnn_conf:.2f}) "
|
|
|
|
| 249 |
f"Votes ai={ai_votes} real={real_votes}/{total_weight} "
|
| 250 |
f"β {final_label} ({final_confidence:.1f}%)"
|
| 251 |
)
|
| 252 |
|
| 253 |
return final_label, round(final_confidence, 2)
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
def _predict_hf1(pil_image: Image.Image) -> str:
|
| 144 |
"""Deepfake-Detection-Exp-02-21 β labels: 'Deepfake' / 'Real' (correct)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
try:
|
| 146 |
+
if _model_hf1 is None:
|
| 147 |
+
_load_hf_models()
|
| 148 |
+
if not _model_hf1:
|
| 149 |
+
return "unknown"
|
| 150 |
best = max(_model_hf1(pil_image), key=lambda x: x["score"])
|
| 151 |
return "ai" if best["label"].lower() == "deepfake" else "real"
|
| 152 |
+
except Exception as exc:
|
| 153 |
+
logger.warning(f"[HF1] Inference failed (non-fatal): {exc}")
|
| 154 |
return "unknown"
|
| 155 |
|
| 156 |
|
| 157 |
def _predict_hf2(pil_image: Image.Image) -> str:
|
| 158 |
"""Deep-Fake-Detector-v2-Model β labels are INVERTED in HF config:
|
| 159 |
'Realism' actually means Deepfake, 'Deepfake' actually means Real."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
try:
|
| 161 |
+
if _model_hf2 is None:
|
| 162 |
+
_load_hf_models()
|
| 163 |
+
if not _model_hf2:
|
| 164 |
+
return "unknown"
|
| 165 |
best = max(_model_hf2(pil_image), key=lambda x: x["score"])
|
| 166 |
# Inverted: 'Realism' label β AI fake
|
| 167 |
return "ai" if best["label"].lower() == "realism" else "real"
|
| 168 |
+
except Exception as exc:
|
| 169 |
+
logger.warning(f"[HF2] Inference failed (non-fatal): {exc}")
|
| 170 |
return "unknown"
|
| 171 |
|
| 172 |
|
|
|
|
| 192 |
β In case of a tie, CNN label wins (it is the tiebreaker).
|
| 193 |
|
| 194 |
Confidence = (winning_weighted_votes / total_weighted_votes) * 100
|
| 195 |
+
|
| 196 |
+
This function is designed to NEVER raise. Any failure in the HF
|
| 197 |
+
models or the threading layer falls back to CNN-only inference.
|
| 198 |
"""
|
| 199 |
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout
|
| 200 |
|
| 201 |
image = _load_image(image_source)
|
| 202 |
|
| 203 |
+
# ββ Attempt concurrent 3-model inference βββββββββββββββββββββββββββββββββ
|
| 204 |
+
probability = None
|
| 205 |
+
hf1_label = "unknown"
|
| 206 |
+
hf2_label = "unknown"
|
|
|
|
| 207 |
|
| 208 |
+
try:
|
| 209 |
+
with ThreadPoolExecutor(max_workers=3) as ex:
|
| 210 |
+
fut_cnn = ex.submit(_predict_probability, image)
|
| 211 |
+
fut_hf1 = ex.submit(_predict_hf1, image)
|
| 212 |
+
fut_hf2 = ex.submit(_predict_hf2, image)
|
| 213 |
+
|
| 214 |
+
# CNN must succeed β give it 30 s; HF models get 60 s each
|
| 215 |
+
probability = fut_cnn.result(timeout=30)
|
| 216 |
+
|
| 217 |
+
try:
|
| 218 |
+
hf1_label = fut_hf1.result(timeout=60)
|
| 219 |
+
except Exception as hf1_err:
|
| 220 |
+
logger.warning(f"[HF1] result() raised (non-fatal): {hf1_err}")
|
| 221 |
+
hf1_label = "unknown"
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
hf2_label = fut_hf2.result(timeout=60)
|
| 225 |
+
except Exception as hf2_err:
|
| 226 |
+
logger.warning(f"[HF2] result() raised (non-fatal): {hf2_err}")
|
| 227 |
+
hf2_label = "unknown"
|
| 228 |
+
|
| 229 |
+
except Exception as pool_err:
|
| 230 |
+
# Entire thread pool failed (e.g. CNN itself threw). Fall back to
|
| 231 |
+
# a direct synchronous CNN call so we always return something.
|
| 232 |
+
logger.error(f"ThreadPoolExecutor failed, falling back to CNN-only: {pool_err}")
|
| 233 |
+
try:
|
| 234 |
+
probability = _predict_probability(image)
|
| 235 |
+
except Exception as cnn_err:
|
| 236 |
+
# Absolute last resort β model is broken; return a neutral result
|
| 237 |
+
logger.error(f"CNN fallback also failed: {cnn_err}")
|
| 238 |
+
return "real", 50.0
|
| 239 |
|
| 240 |
# ββ CNN decision ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 241 |
cnn_label = "real" if probability > OPTIMAL_THRESHOLD else "ai"
|
|
|
|
| 276 |
cnn_confidence_pct = cnn_conf * 100
|
| 277 |
final_confidence = (vote_confidence * 0.6) + (cnn_confidence_pct * 0.4)
|
| 278 |
|
| 279 |
+
logger.info(
|
| 280 |
f"CNN={cnn_label}({cnn_conf:.2f}) "
|
| 281 |
+
f"HF1={hf1_label} HF2={hf2_label} "
|
| 282 |
f"Votes ai={ai_votes} real={real_votes}/{total_weight} "
|
| 283 |
f"β {final_label} ({final_confidence:.1f}%)"
|
| 284 |
)
|
| 285 |
|
| 286 |
return final_label, round(final_confidence, 2)
|
| 287 |
+
|
| 288 |
+
|