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
Files changed (2) hide show
  1. backend/detector.py +29 -28
  2. 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
- True batched inference β€” all crops in ONE forward pass per model.
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
- # Convert all crops to PIL once
 
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
- # Batch process all images at once
506
- inputs = proc(images=pil_imgs, return_tensors="pt")
507
-
508
- # Convert to float16 if model is float16
509
- if next(model.parameters()).dtype == torch.float16:
510
- inputs = {
511
- k: v.half() if v.dtype == torch.float32 else v
512
- for k, v in inputs.items()
513
- }
514
-
515
- with torch.no_grad():
516
- logits = model(**inputs).logits # [N, classes]
517
- probs = torch.softmax(logits, dim=-1) # [N, classes]
518
- scores = probs[:, fake_idx].tolist() # [N]
519
-
520
- all_model_scores.append(scores)
521
-
522
- # Early exit: if model 1 is very confident on ALL crops, skip model 2
 
 
 
 
 
523
  if model_idx == 0:
524
- model1_scores = scores
525
- avg = sum(scores) / len(scores)
526
  if avg > 0.88 or avg < 0.12:
527
- logger.info(f"Early exit: model1 avg={avg:.3f}, skipping model2")
528
  break
529
 
530
  except Exception as e:
531
- logger.warning(f"Batch inference error model {model_idx}: {e}")
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
- Analyze an uploaded video for deepfake content.
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
- # Use fast mode only for short extension captures (< 30s), full mode for uploaded files
299
- video_meta = None
300
  try:
301
- import cv2
302
- cap = cv2.VideoCapture(str(analyze_path))
303
- fps = cap.get(cv2.CAP_PROP_FPS)
304
- total = cap.get(cv2.CAP_PROP_FRAME_COUNT)
305
- cap.release()
306
- duration = total / fps if fps > 0 else 999
307
  except Exception:
308
  duration = 999
309
- fast = duration < 30 # extension captures are ~8s; uploaded files are longer
310
  logger.info(f"Video duration: {duration:.1f}s β†’ fast_mode={fast}")
311
- result = authenticator.analyze(str(analyze_path), fast_mode=fast)
 
 
 
 
 
 
 
 
 
 
 
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: