Deepfake Authenticator commited on
Commit
c40bdaa
Β·
1 Parent(s): 6dc8e68

fix: Improve accuracy - remove phone bias, raise threshold to 0.65, apply 10% conservative bias to ensemble

Browse files
Files changed (1) hide show
  1. backend/detector.py +23 -74
backend/detector.py CHANGED
@@ -217,47 +217,8 @@ class FrameAnalyzerAgent:
217
  "height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
218
  }
219
  meta["duration_sec"] = round(meta["total_frames"] / meta["fps"], 2) if meta["fps"] > 0 else 0
220
-
221
- # Detect phone video characteristics
222
- meta["is_phone_video"] = self._detect_phone_video(meta)
223
-
224
  cap.release()
225
  return meta
226
-
227
- def _detect_phone_video(self, meta: dict) -> bool:
228
- """
229
- Detect if video is likely from a phone camera based on resolution and aspect ratio.
230
- Phone videos typically have:
231
- - Vertical orientation (9:16) or square (1:1)
232
- - Common phone resolutions: 1080x1920, 720x1280, 1080x1080
233
- - 30fps or 60fps (not 24fps or 25fps which are professional)
234
- """
235
- width = meta.get("width", 0)
236
- height = meta.get("height", 0)
237
- fps = meta.get("fps", 0)
238
-
239
- if width == 0 or height == 0:
240
- return False
241
-
242
- aspect_ratio = width / height
243
-
244
- # Vertical video (portrait mode)
245
- if aspect_ratio < 0.75: # More vertical than 4:3
246
- return True
247
-
248
- # Square video (Instagram/Snapchat)
249
- if 0.95 <= aspect_ratio <= 1.05:
250
- return True
251
-
252
- # Common phone resolutions
253
- phone_resolutions = [
254
- (1080, 1920), (720, 1280), (1080, 1080),
255
- (1920, 1080), (1280, 720), # Landscape phone
256
- ]
257
- if (width, height) in phone_resolutions or (height, width) in phone_resolutions:
258
- return True
259
-
260
- return False
261
 
262
 
263
  # ─────────────────────────────────────────────
@@ -489,9 +450,14 @@ class DecisionAgent:
489
  if not fake_probs:
490
  results.append(self._heuristic_predict(crop))
491
  elif len(fake_probs) == 2:
492
- results.append(fake_probs[0] * 0.55 + fake_probs[1] * 0.45)
 
 
 
 
 
493
  else:
494
- results.append(float(np.mean(fake_probs)))
495
 
496
  return results
497
 
@@ -663,7 +629,7 @@ class DecisionAgent:
663
  # Agent 4: Report Generator Agent
664
  # ─────────────────────────────────────────────
665
  class ReportGeneratorAgent:
666
- BASE_THRESHOLD = 0.58
667
 
668
  def generate(self, analysis: dict, metadata: dict,
669
  audio: dict | None = None,
@@ -672,25 +638,13 @@ class ReportGeneratorAgent:
672
  prob = analysis["overall_fake_probability"]
673
  consistency = analysis.get("consistency", 0.5)
674
  coverage = analysis.get("face_coverage", 0.5)
675
-
676
- # Phone video bias correction
677
- is_phone = metadata.get("is_phone_video", False)
678
- if is_phone:
679
- # Phone videos tend to score higher on fake probability due to:
680
- # - Heavy AI processing (HDR, beauty mode, noise reduction)
681
- # - Different compression artifacts
682
- # - Lower quality sensors
683
- # Apply a bias correction to reduce false positives
684
- original_prob = prob
685
- prob = prob * 0.85 # Reduce by 15%
686
- logger.info(f"Phone video detected: adjusted prob {original_prob:.3f} β†’ {prob:.3f}")
687
 
688
  # ── C2PA hard override ────────────────────────────────────────────
689
  if metadata_result and metadata_result.get("is_ai_generated"):
690
  is_fake = True
691
  calibrated = self._calibrate(max(prob, 0.80))
692
  details = self._build_details(analysis, metadata, prob, True,
693
- self.BASE_THRESHOLD, metadata_result, is_phone)
694
  return {
695
  "result": "FAKE",
696
  "confidence": round(calibrated * 100, 1),
@@ -702,22 +656,21 @@ class ReportGeneratorAgent:
702
  "video_duration_sec": metadata.get("duration_sec", 0),
703
  "video_fps": metadata.get("fps", 0),
704
  "resolution": f"{metadata.get('width',0)}x{metadata.get('height',0)}",
705
- "is_phone_video": is_phone,
706
  },
707
  }
708
 
709
  # ── Adaptive threshold ─────────────────────────���──────────────────
710
  threshold = self.BASE_THRESHOLD
711
- if consistency >= 0.70 and coverage >= 0.50:
712
- threshold -= 0.06
713
- elif consistency >= 0.55:
714
- threshold -= 0.03
715
- elif consistency < 0.35:
716
- threshold += 0.07
717
 
718
- # Additional threshold adjustment for phone videos
719
- if is_phone:
720
- threshold += 0.08 # Raise threshold to reduce false positives
 
 
 
 
 
 
721
 
722
  visual_fake = prob >= threshold
723
 
@@ -736,7 +689,8 @@ class ReportGeneratorAgent:
736
  elif not visual_fake and not audio_fake:
737
  is_fake = False
738
  elif visual_fake and not audio_fake:
739
- is_fake = prob >= (threshold + 0.05)
 
740
  else:
741
  is_fake = audio_prob >= 0.75
742
  calibrated = self._calibrate(prob)
@@ -746,9 +700,9 @@ class ReportGeneratorAgent:
746
 
747
  confidence = round(calibrated * 100, 1)
748
  result = "FAKE" if is_fake else "REAL"
749
- logger.info(f"Decision: prob={prob:.3f} threshold={threshold:.3f} phone={is_phone} β†’ {result}")
750
 
751
- details = self._build_details(analysis, metadata, prob, is_fake, threshold, None, is_phone)
752
  frame_timeline = self._build_timeline(analysis.get("frame_scores", []))
753
 
754
  return {
@@ -760,7 +714,6 @@ class ReportGeneratorAgent:
760
  "video_duration_sec": metadata.get("duration_sec", 0),
761
  "video_fps": metadata.get("fps", 0),
762
  "resolution": f"{metadata.get('width',0)}x{metadata.get('height',0)}",
763
- "is_phone_video": is_phone,
764
  },
765
  }
766
 
@@ -772,7 +725,7 @@ class ReportGeneratorAgent:
772
  return float(np.clip(conf, 0.88, 0.99))
773
 
774
  def _build_details(self, analysis, metadata, prob, is_fake,
775
- threshold=0.58, metadata_result=None, is_phone=False) -> list[str]:
776
  details = []
777
  frame_scores = analysis.get("frame_scores", [])
778
  frames_with_faces = analysis.get("frames_with_faces", 0)
@@ -816,10 +769,6 @@ class ReportGeneratorAgent:
816
  else:
817
  details.append("Video appears authentic β€” deepfake probability below detection threshold")
818
 
819
- # Add phone video context for authentic videos
820
- if is_phone:
821
- details.append("πŸ“± Phone camera detected β€” analysis adjusted for mobile video characteristics")
822
-
823
  details.append("Natural facial texture and lighting consistency observed across frames")
824
  details.append("Compression artifacts consistent with genuine camera-captured footage")
825
  if frames_with_faces > 0:
 
217
  "height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
218
  }
219
  meta["duration_sec"] = round(meta["total_frames"] / meta["fps"], 2) if meta["fps"] > 0 else 0
 
 
 
 
220
  cap.release()
221
  return meta
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
 
224
  # ─────────────────────────────────────────────
 
450
  if not fake_probs:
451
  results.append(self._heuristic_predict(crop))
452
  elif len(fake_probs) == 2:
453
+ # Weighted ensemble: give more weight to first model, less to second
454
+ # Reduce overall sensitivity to prevent false positives
455
+ ensemble_score = fake_probs[0] * 0.50 + fake_probs[1] * 0.40
456
+ # Apply conservative bias - shift scores toward "real"
457
+ ensemble_score = ensemble_score * 0.90
458
+ results.append(ensemble_score)
459
  else:
460
+ results.append(float(np.mean(fake_probs)) * 0.90)
461
 
462
  return results
463
 
 
629
  # Agent 4: Report Generator Agent
630
  # ─────────────────────────────────────────────
631
  class ReportGeneratorAgent:
632
+ BASE_THRESHOLD = 0.65 # Raised from 0.58 to reduce false positives
633
 
634
  def generate(self, analysis: dict, metadata: dict,
635
  audio: dict | None = None,
 
638
  prob = analysis["overall_fake_probability"]
639
  consistency = analysis.get("consistency", 0.5)
640
  coverage = analysis.get("face_coverage", 0.5)
 
 
 
 
 
 
 
 
 
 
 
 
641
 
642
  # ── C2PA hard override ────────────────────────────────────────────
643
  if metadata_result and metadata_result.get("is_ai_generated"):
644
  is_fake = True
645
  calibrated = self._calibrate(max(prob, 0.80))
646
  details = self._build_details(analysis, metadata, prob, True,
647
+ self.BASE_THRESHOLD, metadata_result)
648
  return {
649
  "result": "FAKE",
650
  "confidence": round(calibrated * 100, 1),
 
656
  "video_duration_sec": metadata.get("duration_sec", 0),
657
  "video_fps": metadata.get("fps", 0),
658
  "resolution": f"{metadata.get('width',0)}x{metadata.get('height',0)}",
 
659
  },
660
  }
661
 
662
  # ── Adaptive threshold ─────────────────────────���──────────────────
663
  threshold = self.BASE_THRESHOLD
 
 
 
 
 
 
664
 
665
+ # More conservative thresholds based on consistency
666
+ if consistency >= 0.75 and coverage >= 0.60:
667
+ # Very high consistency - can be slightly more aggressive
668
+ threshold -= 0.04
669
+ elif consistency >= 0.60:
670
+ threshold -= 0.02
671
+ elif consistency < 0.40:
672
+ # Low consistency - be more conservative
673
+ threshold += 0.10
674
 
675
  visual_fake = prob >= threshold
676
 
 
689
  elif not visual_fake and not audio_fake:
690
  is_fake = False
691
  elif visual_fake and not audio_fake:
692
+ # Visual says fake but audio says real - require higher confidence
693
+ is_fake = prob >= (threshold + 0.08)
694
  else:
695
  is_fake = audio_prob >= 0.75
696
  calibrated = self._calibrate(prob)
 
700
 
701
  confidence = round(calibrated * 100, 1)
702
  result = "FAKE" if is_fake else "REAL"
703
+ logger.info(f"Decision: prob={prob:.3f} threshold={threshold:.3f} β†’ {result}")
704
 
705
+ details = self._build_details(analysis, metadata, prob, is_fake, threshold)
706
  frame_timeline = self._build_timeline(analysis.get("frame_scores", []))
707
 
708
  return {
 
714
  "video_duration_sec": metadata.get("duration_sec", 0),
715
  "video_fps": metadata.get("fps", 0),
716
  "resolution": f"{metadata.get('width',0)}x{metadata.get('height',0)}",
 
717
  },
718
  }
719
 
 
725
  return float(np.clip(conf, 0.88, 0.99))
726
 
727
  def _build_details(self, analysis, metadata, prob, is_fake,
728
+ threshold=0.65, metadata_result=None) -> list[str]:
729
  details = []
730
  frame_scores = analysis.get("frame_scores", [])
731
  frames_with_faces = analysis.get("frames_with_faces", 0)
 
769
  else:
770
  details.append("Video appears authentic β€” deepfake probability below detection threshold")
771
 
 
 
 
 
772
  details.append("Natural facial texture and lighting consistency observed across frames")
773
  details.append("Compression artifacts consistent with genuine camera-captured footage")
774
  if frames_with_faces > 0: