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- 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 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 231 |
else:
|
| 232 |
-
scores.append(0.
|
| 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 |
-
|
|
|
|
|
|
|
| 243 |
diff_mean = float(np.mean(diffs))
|
| 244 |
-
diff_cv =
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 254 |
else:
|
| 255 |
-
scores.append(0.
|
| 256 |
|
| 257 |
-
# ── 3.
|
| 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 |
-
|
| 264 |
-
blur
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 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
|
| 276 |
-
|
| 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 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
mean_drift = float(np.mean(
|
| 287 |
-
if mean_drift >
|
| 288 |
-
scores.append(0.
|
| 289 |
-
signals.append(
|
| 290 |
else:
|
| 291 |
scores.append(0.28)
|
| 292 |
|
| 293 |
except Exception as e:
|
| 294 |
-
logger.warning(
|
| 295 |
return {"score": 0.5, "available": False, "signals": []}
|
| 296 |
|
| 297 |
final_score = float(np.mean(scores)) if scores else 0.5
|
| 298 |
-
logger.info(
|
| 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.
|
| 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 |
-
#
|
| 757 |
-
|
| 758 |
-
|
|
|
|
| 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 |
|