Deepfake Authenticator commited on
Commit
4d23473
·
1 Parent(s): 12fd879

fix: raise threshold to 0.62, conservative temporal thresholds — fix false positives on real phone videos

Browse files
Files changed (1) hide show
  1. backend/detector.py +51 -65
backend/detector.py CHANGED
@@ -208,94 +208,79 @@ class TemporalConsistencyAgent:
208
  if len(frames) < 4:
209
  return {"score": 0.5, "available": False, "signals": []}
210
 
211
- signals = []
212
- scores = []
213
 
214
  try:
215
- # ── 1. Pixel-level temporal variance ─────────────────────────
216
- # AI video: unnaturally low variance in static regions
217
- # Real video: natural noise/grain causes higher variance
218
  gray_frames = [cv2.cvtColor(f, cv2.COLOR_BGR2GRAY).astype(np.float32)
219
  for f in frames]
220
- stack = np.stack(gray_frames, axis=0) # [N, H, W]
221
- pixel_var = np.mean(np.var(stack, axis=0)) # mean variance per pixel
222
-
223
- # Real video: pixel_var typically 50-300
224
- # AI video: often < 30 (too smooth) or > 500 (flickering)
225
- if pixel_var < 25:
226
- scores.append(0.72)
227
- signals.append(f"Unnaturally smooth temporal texture (var={pixel_var:.1f})")
228
- elif pixel_var > 600:
229
  scores.append(0.68)
230
- signals.append(f"Excessive temporal flickering (var={pixel_var:.1f})")
 
 
 
231
  else:
232
- scores.append(0.30)
233
-
234
- # ── 2. Frame difference consistency ──────────────────────────
235
- # AI video: frame diffs are too uniform (generated at fixed rate)
236
- # Real video: natural motion causes variable frame differences
237
- diffs = []
238
- for i in range(1, len(gray_frames)):
239
- diff = np.mean(np.abs(gray_frames[i] - gray_frames[i-1]))
240
- diffs.append(diff)
241
 
242
- diff_std = float(np.std(diffs))
 
 
243
  diff_mean = float(np.mean(diffs))
244
- diff_cv = diff_std / (diff_mean + 1e-8) # coefficient of variation
245
-
246
- # Real video: CV typically 0.3-0.8 (variable motion)
247
- # AI video: CV often < 0.15 (too uniform) or > 1.2 (unstable)
248
- if diff_cv < 0.12:
249
- scores.append(0.70)
250
- signals.append(f"Unnaturally uniform motion pattern (CV={diff_cv:.3f})")
251
- elif diff_cv > 1.3:
252
  scores.append(0.65)
253
- signals.append(f"Unstable frame transitions (CV={diff_cv:.3f})")
 
 
 
254
  else:
255
- scores.append(0.28)
256
 
257
- # ── 3. High-frequency temporal noise ─────────────────────────
258
- # Real cameras have consistent sensor noise patterns
259
- # AI generators produce different noise each frame
260
  if len(frames) >= 6:
261
  noise_vars = []
262
  for frame in frames:
263
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY).astype(np.float32)
264
- blur = cv2.GaussianBlur(gray, (5, 5), 0)
265
- noise = gray - blur
266
- noise_vars.append(float(np.var(noise)))
267
-
268
- noise_consistency = float(np.std(noise_vars) / (np.mean(noise_vars) + 1e-8))
269
- if noise_consistency > 0.5:
270
- scores.append(0.66)
271
- signals.append(f"Inconsistent noise pattern across frames ({noise_consistency:.2f})")
272
  else:
273
  scores.append(0.30)
274
 
275
- # ── 4. Color channel temporal stability ───────────────────────
276
- # AI video often has subtle color shifts between frames
277
- channel_drifts = []
278
  for i in range(1, min(len(frames), 15)):
279
  b1, g1, r1 = cv2.split(frames[i-1].astype(np.float32))
280
  b2, g2, r2 = cv2.split(frames[i].astype(np.float32))
281
- drift = abs(np.mean(r1) - np.mean(r2)) + \
282
- abs(np.mean(g1) - np.mean(g2)) + \
283
- abs(np.mean(b1) - np.mean(b2))
284
- channel_drifts.append(drift)
285
-
286
- mean_drift = float(np.mean(channel_drifts))
287
- if mean_drift > 8.0:
288
- scores.append(0.68)
289
- signals.append(f"Color channel drift between frames ({mean_drift:.1f})")
290
  else:
291
  scores.append(0.28)
292
 
293
  except Exception as e:
294
- logger.warning(f"Temporal analysis error: {e}")
295
  return {"score": 0.5, "available": False, "signals": []}
296
 
297
  final_score = float(np.mean(scores)) if scores else 0.5
298
- logger.info(f"Temporal score: {final_score:.3f} signals={signals}")
299
 
300
  return {
301
  "score": round(final_score, 4),
@@ -716,7 +701,7 @@ class DecisionAgent:
716
  # Agent 4: Report Generator Agent
717
  # ─────────────────────────────────────────────
718
  class ReportGeneratorAgent:
719
- BASE_THRESHOLD = 0.58 # Restored 0.54 caused false positives
720
 
721
  def generate(self, analysis: dict, metadata: dict, audio: dict | None = None,
722
  metadata_result: dict | None = None, temporal_result: dict | None = None) -> dict:
@@ -753,9 +738,10 @@ class ReportGeneratorAgent:
753
  temporal_score = 0.5
754
  if temporal_result and temporal_result.get("available"):
755
  temporal_score = temporal_result["score"]
756
- # Blend temporal into visual probability (20% weight)
757
- if temporal_score > 0.60:
758
- prob = prob * 0.80 + temporal_score * 0.20
 
759
  prob = round(float(np.clip(prob, 0.0, 1.0)), 4)
760
  logger.info(f"Temporal boost applied: new prob={prob:.3f}")
761
 
 
208
  if len(frames) < 4:
209
  return {"score": 0.5, "available": False, "signals": []}
210
 
211
+ signals = []
212
+ scores = []
213
 
214
  try:
 
 
 
215
  gray_frames = [cv2.cvtColor(f, cv2.COLOR_BGR2GRAY).astype(np.float32)
216
  for f in frames]
217
+
218
+ # ── 1. Pixel variance only flag near-zero (AI renders perfectly still) ──
219
+ stack = np.stack(gray_frames, axis=0)
220
+ pixel_var = float(np.mean(np.var(stack, axis=0)))
221
+ if pixel_var < 3.0:
222
+ # Essentially zero variance — only AI generators produce this
 
 
 
223
  scores.append(0.68)
224
+ signals.append("Near-zero pixel variance — AI-generated stillness")
225
+ elif pixel_var > 900:
226
+ scores.append(0.62)
227
+ signals.append("Extreme temporal flickering")
228
  else:
229
+ scores.append(0.32) # neutral — real phone videos land here
 
 
 
 
 
 
 
 
230
 
231
+ # ── 2. Frame diff CV — only flag essentially zero (perfectly uniform) ──
232
+ diffs = [float(np.mean(np.abs(gray_frames[i] - gray_frames[i-1])))
233
+ for i in range(1, len(gray_frames))]
234
  diff_mean = float(np.mean(diffs))
235
+ diff_cv = float(np.std(diffs)) / (diff_mean + 1e-8)
236
+
237
+ if diff_cv < 0.008:
238
+ # Perfectly identical diffs only AI produces this
 
 
 
 
239
  scores.append(0.65)
240
+ signals.append("Perfectly uniform frame transitions — AI pattern")
241
+ elif diff_cv > 2.0:
242
+ scores.append(0.60)
243
+ signals.append("Highly erratic frame transitions")
244
  else:
245
+ scores.append(0.30) # neutral
246
 
247
+ # ── 3. Noise consistency only flag extreme inconsistency ────
 
 
248
  if len(frames) >= 6:
249
  noise_vars = []
250
  for frame in frames:
251
+ g = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY).astype(np.float32)
252
+ blur = cv2.GaussianBlur(g, (5, 5), 0)
253
+ noise_vars.append(float(np.var(g - blur)))
254
+ nc = float(np.std(noise_vars) / (np.mean(noise_vars) + 1e-8))
255
+ if nc > 1.0:
256
+ scores.append(0.62)
257
+ signals.append("Highly inconsistent sensor noise pattern")
 
 
258
  else:
259
  scores.append(0.30)
260
 
261
+ # ── 4. Color drift only flag severe drift ───────────────────
262
+ drifts = []
 
263
  for i in range(1, min(len(frames), 15)):
264
  b1, g1, r1 = cv2.split(frames[i-1].astype(np.float32))
265
  b2, g2, r2 = cv2.split(frames[i].astype(np.float32))
266
+ drifts.append(
267
+ abs(float(np.mean(r1)) - float(np.mean(r2))) +
268
+ abs(float(np.mean(g1)) - float(np.mean(g2))) +
269
+ abs(float(np.mean(b1)) - float(np.mean(b2)))
270
+ )
271
+ mean_drift = float(np.mean(drifts))
272
+ if mean_drift > 20.0:
273
+ scores.append(0.63)
274
+ signals.append("Severe color channel drift between frames")
275
  else:
276
  scores.append(0.28)
277
 
278
  except Exception as e:
279
+ logger.warning("Temporal analysis error: %s", e)
280
  return {"score": 0.5, "available": False, "signals": []}
281
 
282
  final_score = float(np.mean(scores)) if scores else 0.5
283
+ logger.info("Temporal score: %.3f signals=%s", final_score, signals)
284
 
285
  return {
286
  "score": round(final_score, 4),
 
701
  # Agent 4: Report Generator Agent
702
  # ─────────────────────────────────────────────
703
  class ReportGeneratorAgent:
704
+ BASE_THRESHOLD = 0.62 # Raised from 0.58 to reduce false positives on real phone videos
705
 
706
  def generate(self, analysis: dict, metadata: dict, audio: dict | None = None,
707
  metadata_result: dict | None = None, temporal_result: dict | None = None) -> dict:
 
738
  temporal_score = 0.5
739
  if temporal_result and temporal_result.get("available"):
740
  temporal_score = temporal_result["score"]
741
+ # Only boost if temporal is strongly suspicious (> 0.65) AND
742
+ # visual model already leans fake (> 0.45) — prevents false positives
743
+ if temporal_score > 0.65 and prob > 0.45:
744
+ prob = prob * 0.85 + temporal_score * 0.15 # reduced from 0.20
745
  prob = round(float(np.clip(prob, 0.0, 1.0)), 4)
746
  logger.info(f"Temporal boost applied: new prob={prob:.3f}")
747