Deepfake Authenticator commited on
Commit Β·
695c9b0
1
Parent(s): 470a637
fix: micro-batch inference (8 at a time), 120s timeout, prevent OOM hang on HF CPU
Browse files- backend/detector.py +29 -28
- backend/main.py +22 -14
backend/detector.py
CHANGED
|
@@ -481,8 +481,7 @@ class DecisionAgent:
|
|
| 481 |
|
| 482 |
def _batch_predict(self, face_crops: list[np.ndarray]) -> list[float]:
|
| 483 |
"""
|
| 484 |
-
|
| 485 |
-
Float16 + batching = ~4Γ faster than original per-crop float32.
|
| 486 |
Early exit: skip model 2 if model 1 is already very confident.
|
| 487 |
"""
|
| 488 |
if not face_crops:
|
|
@@ -491,51 +490,53 @@ class DecisionAgent:
|
|
| 491 |
from PIL import Image
|
| 492 |
import torch
|
| 493 |
|
| 494 |
-
#
|
|
|
|
| 495 |
pil_imgs = [
|
| 496 |
Image.fromarray(cv2.cvtColor(c, cv2.COLOR_BGR2RGB))
|
| 497 |
for c in face_crops
|
| 498 |
]
|
| 499 |
|
| 500 |
-
model1_scores = None
|
| 501 |
all_model_scores = []
|
| 502 |
|
| 503 |
for model_idx, (proc, model, fake_idx) in enumerate(self.models):
|
| 504 |
try:
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
if model_idx == 0:
|
| 524 |
-
|
| 525 |
-
avg = sum(scores) / len(scores)
|
| 526 |
if avg > 0.88 or avg < 0.12:
|
| 527 |
-
logger.info(
|
| 528 |
break
|
| 529 |
|
| 530 |
except Exception as e:
|
| 531 |
-
logger.warning(
|
| 532 |
-
# Fallback to heuristic for this model
|
| 533 |
all_model_scores.append([self._heuristic_predict(c) for c in face_crops])
|
| 534 |
|
| 535 |
if not all_model_scores:
|
| 536 |
return [self._heuristic_predict(c) for c in face_crops]
|
| 537 |
|
| 538 |
-
# Ensemble: weighted average across models per crop
|
| 539 |
n = len(face_crops)
|
| 540 |
if len(all_model_scores) == 1:
|
| 541 |
return all_model_scores[0]
|
|
|
|
| 481 |
|
| 482 |
def _batch_predict(self, face_crops: list[np.ndarray]) -> list[float]:
|
| 483 |
"""
|
| 484 |
+
Micro-batched inference β process 8 crops at a time to avoid OOM on CPU.
|
|
|
|
| 485 |
Early exit: skip model 2 if model 1 is already very confident.
|
| 486 |
"""
|
| 487 |
if not face_crops:
|
|
|
|
| 490 |
from PIL import Image
|
| 491 |
import torch
|
| 492 |
|
| 493 |
+
MICRO_BATCH = 8 # safe for 2GB RAM CPU inference
|
| 494 |
+
|
| 495 |
pil_imgs = [
|
| 496 |
Image.fromarray(cv2.cvtColor(c, cv2.COLOR_BGR2RGB))
|
| 497 |
for c in face_crops
|
| 498 |
]
|
| 499 |
|
|
|
|
| 500 |
all_model_scores = []
|
| 501 |
|
| 502 |
for model_idx, (proc, model, fake_idx) in enumerate(self.models):
|
| 503 |
try:
|
| 504 |
+
model_scores = []
|
| 505 |
+
# Process in micro-batches
|
| 506 |
+
for i in range(0, len(pil_imgs), MICRO_BATCH):
|
| 507 |
+
batch = pil_imgs[i:i + MICRO_BATCH]
|
| 508 |
+
inputs = proc(images=batch, return_tensors="pt")
|
| 509 |
+
|
| 510 |
+
# Match model dtype
|
| 511 |
+
model_dtype = next(model.parameters()).dtype
|
| 512 |
+
if model_dtype == torch.float16:
|
| 513 |
+
inputs = {
|
| 514 |
+
k: v.half() if v.dtype == torch.float32 else v
|
| 515 |
+
for k, v in inputs.items()
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
with torch.no_grad():
|
| 519 |
+
logits = model(**inputs).logits
|
| 520 |
+
probs = torch.softmax(logits.float(), dim=-1)
|
| 521 |
+
scores = probs[:, fake_idx].tolist()
|
| 522 |
+
model_scores.extend(scores)
|
| 523 |
+
|
| 524 |
+
all_model_scores.append(model_scores)
|
| 525 |
+
|
| 526 |
+
# Early exit: model 1 very confident β skip model 2
|
| 527 |
if model_idx == 0:
|
| 528 |
+
avg = sum(model_scores) / len(model_scores)
|
|
|
|
| 529 |
if avg > 0.88 or avg < 0.12:
|
| 530 |
+
logger.info("Early exit: model1 avg=%.3f, skipping model2", avg)
|
| 531 |
break
|
| 532 |
|
| 533 |
except Exception as e:
|
| 534 |
+
logger.warning("Batch inference error model %d: %s", model_idx, e)
|
|
|
|
| 535 |
all_model_scores.append([self._heuristic_predict(c) for c in face_crops])
|
| 536 |
|
| 537 |
if not all_model_scores:
|
| 538 |
return [self._heuristic_predict(c) for c in face_crops]
|
| 539 |
|
|
|
|
| 540 |
n = len(face_crops)
|
| 541 |
if len(all_model_scores) == 1:
|
| 542 |
return all_model_scores[0]
|
backend/main.py
CHANGED
|
@@ -234,10 +234,8 @@ async def analyze_video(
|
|
| 234 |
file: UploadFile = File(...),
|
| 235 |
x_api_key: Optional[str] = Header(None, alias="X-API-Key")
|
| 236 |
):
|
| 237 |
-
"""
|
| 238 |
-
|
| 239 |
-
Requires API key for usage tracking and tier limits.
|
| 240 |
-
"""
|
| 241 |
# Check API key (allow localhost without key for development)
|
| 242 |
if x_api_key:
|
| 243 |
key_data = validate_api_key(x_api_key)
|
|
@@ -295,20 +293,30 @@ async def analyze_video(
|
|
| 295 |
logger.info(f"File is {suffix} β no conversion needed")
|
| 296 |
|
| 297 |
logger.info(f"Calling authenticator.analyze({analyze_path})")
|
| 298 |
-
#
|
| 299 |
-
video_meta = None
|
| 300 |
try:
|
| 301 |
-
import cv2
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
duration =
|
| 307 |
except Exception:
|
| 308 |
duration = 999
|
| 309 |
-
fast = duration < 30
|
| 310 |
logger.info(f"Video duration: {duration:.1f}s β fast_mode={fast}")
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
# Increment usage counter if API key provided
|
| 314 |
if x_api_key:
|
|
|
|
| 234 |
file: UploadFile = File(...),
|
| 235 |
x_api_key: Optional[str] = Header(None, alias="X-API-Key")
|
| 236 |
):
|
| 237 |
+
"""Analyze an uploaded video for deepfake content."""
|
| 238 |
+
import asyncio
|
|
|
|
|
|
|
| 239 |
# Check API key (allow localhost without key for development)
|
| 240 |
if x_api_key:
|
| 241 |
key_data = validate_api_key(x_api_key)
|
|
|
|
| 293 |
logger.info(f"File is {suffix} β no conversion needed")
|
| 294 |
|
| 295 |
logger.info(f"Calling authenticator.analyze({analyze_path})")
|
| 296 |
+
# Detect duration for fast_mode
|
|
|
|
| 297 |
try:
|
| 298 |
+
import cv2 as _cv2
|
| 299 |
+
_cap = _cv2.VideoCapture(str(analyze_path))
|
| 300 |
+
_fps = _cap.get(_cv2.CAP_PROP_FPS)
|
| 301 |
+
_tot = _cap.get(_cv2.CAP_PROP_FRAME_COUNT)
|
| 302 |
+
_cap.release()
|
| 303 |
+
duration = _tot / _fps if _fps > 0 else 999
|
| 304 |
except Exception:
|
| 305 |
duration = 999
|
| 306 |
+
fast = duration < 30
|
| 307 |
logger.info(f"Video duration: {duration:.1f}s β fast_mode={fast}")
|
| 308 |
+
|
| 309 |
+
# Run with 120s timeout β never hang forever
|
| 310 |
+
import asyncio, concurrent.futures as _cf
|
| 311 |
+
loop = asyncio.get_event_loop()
|
| 312 |
+
with _cf.ThreadPoolExecutor(max_workers=1) as pool:
|
| 313 |
+
try:
|
| 314 |
+
result = await asyncio.wait_for(
|
| 315 |
+
loop.run_in_executor(pool, lambda: authenticator.analyze(str(analyze_path), fast_mode=fast)),
|
| 316 |
+
timeout=120.0
|
| 317 |
+
)
|
| 318 |
+
except asyncio.TimeoutError:
|
| 319 |
+
raise HTTPException(status_code=504, detail="Analysis timed out after 120s. Try a shorter video.")
|
| 320 |
|
| 321 |
# Increment usage counter if API key provided
|
| 322 |
if x_api_key:
|