shadow55gh commited on
Commit
c5ec583
Β·
1 Parent(s): 6469706

fix: real ELA heatmap, real face boxes, real DCT freq, fix HF URLs

Browse files
backend/main.py CHANGED
@@ -187,10 +187,15 @@ async def analyze(file: UploadFile = File(...), modules: str = "all"):
187
  results["module_scores"][46] = 1.0 - synth_result.get("confidence", 0.5)
188
 
189
  metadata = extract_metadata(content, ct)
 
 
 
 
 
190
  module_scores = results["module_scores"]
191
  custody = results["custody"]
192
  key_findings = results.get("key_findings", [])
193
- ai_summary = f"EfficientNet: {effnet_authentic:.1%} authentic | Combined: {combined:.1%} | {results.get('ai_summary','')}"
194
 
195
  # Visualizations
196
  try:
 
187
  results["module_scores"][46] = 1.0 - synth_result.get("confidence", 0.5)
188
 
189
  metadata = extract_metadata(content, ct)
190
+ # Inject face boxes and ELA data from ML models into metadata
191
+ metadata["faces"] = dl.get("face_boxes", [])
192
+ metadata["face_count"] = dl.get("face_count", 0)
193
+ metadata["ela_data"] = dl.get("ela_data", "")
194
+ metadata["freq_data"] = dl.get("freq_data", {})
195
  module_scores = results["module_scores"]
196
  custody = results["custody"]
197
  key_findings = results.get("key_findings", [])
198
+ ai_summary = f"EfficientNet: {effnet_authentic:.1%} authentic | Combined: {combined:.1%} | faces: {dl.get('face_count',0)} | {results.get('ai_summary','')}"
199
 
200
  # Visualizations
201
  try:
backend/models/deepfake_detector.py CHANGED
@@ -365,14 +365,25 @@ async def analyze_image(content: bytes) -> dict:
365
  total_w = sum(weights_list)
366
  fake_prob = sum(s * w for s, w in zip(scores_list, weights_list)) / total_w
367
 
368
- # ── Face count via MTCNN ─────────────────────────────────
369
  face_count = 0
 
370
  if _mtcnn is not None:
371
  try:
372
- boxes, _ = _mtcnn.detect(img)
373
- face_count = len(boxes) if boxes is not None else 0
374
- except Exception:
375
- pass
 
 
 
 
 
 
 
 
 
 
376
 
377
  logger.debug(
378
  f"[DeepfakeDetector] fake={fake_prob:.3f} effnet={effnet_score:.3f} "
@@ -388,5 +399,76 @@ async def analyze_image(content: bytes) -> dict:
388
  "gan_score": round(gan_score, 4),
389
  "effnet_score": round(effnet_score, 4),
390
  "face_count": face_count,
 
 
 
391
  "method": "+".join(methods),
392
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  total_w = sum(weights_list)
366
  fake_prob = sum(s * w for s, w in zip(scores_list, weights_list)) / total_w
367
 
368
+ # ── Face detection via MTCNN β€” return boxes + count ─────
369
  face_count = 0
370
+ face_boxes = [] # list of [x, y, w, h] in pixel coords
371
  if _mtcnn is not None:
372
  try:
373
+ boxes, probs = _mtcnn.detect(img)
374
+ if boxes is not None:
375
+ face_count = len(boxes)
376
+ for box in boxes:
377
+ x1, y1, x2, y2 = [float(v) for v in box]
378
+ face_boxes.append([x1, y1, x2 - x1, y2 - y1]) # [x, y, w, h]
379
+ except Exception as e:
380
+ logger.debug(f"[MTCNN] detection failed: {e}")
381
+
382
+ # ── Real ELA (Error Level Analysis) heatmap data ─────────
383
+ ela_data = _compute_ela(arr)
384
+
385
+ # ── Frequency domain anomaly score ───────────────────────
386
+ freq_data = _compute_freq_anomaly(arr)
387
 
388
  logger.debug(
389
  f"[DeepfakeDetector] fake={fake_prob:.3f} effnet={effnet_score:.3f} "
 
399
  "gan_score": round(gan_score, 4),
400
  "effnet_score": round(effnet_score, 4),
401
  "face_count": face_count,
402
+ "face_boxes": face_boxes, # [[x,y,w,h], ...] pixel coords
403
+ "ela_data": ela_data, # base64 PNG of ELA map
404
+ "freq_data": freq_data, # {bins: [...], magnitudes: [...]}
405
  "method": "+".join(methods),
406
  }
407
+
408
+
409
+ def _compute_ela(arr: np.ndarray) -> str:
410
+ """
411
+ Real Error Level Analysis β€” compresses image at quality 75,
412
+ subtracts from original, amplifies difference, returns base64 PNG.
413
+ ELA highlights regions that have been re-compressed (edited/generated).
414
+ """
415
+ try:
416
+ import io, base64
417
+ from PIL import Image
418
+
419
+ img_orig = Image.fromarray(arr)
420
+
421
+ # Save at reduced quality
422
+ buf = io.BytesIO()
423
+ img_orig.save(buf, format='JPEG', quality=75)
424
+ buf.seek(0)
425
+ img_compressed = Image.open(buf).convert('RGB')
426
+
427
+ orig_arr = np.array(img_orig, dtype=np.float32)
428
+ comp_arr = np.array(img_compressed, dtype=np.float32)
429
+
430
+ # Difference amplified
431
+ diff = np.abs(orig_arr - comp_arr)
432
+ diff_scaled = np.clip(diff * 15, 0, 255).astype(np.uint8)
433
+
434
+ ela_img = Image.fromarray(diff_scaled)
435
+
436
+ # Resize to reasonable size for frontend
437
+ ela_img = ela_img.resize((320, 240), Image.LANCZOS)
438
+
439
+ out = io.BytesIO()
440
+ ela_img.save(out, format='PNG')
441
+ return base64.b64encode(out.getvalue()).decode('utf-8')
442
+ except Exception as e:
443
+ logger.debug(f"[ELA] computation failed: {e}")
444
+ return ""
445
+
446
+
447
+ def _compute_freq_anomaly(arr: np.ndarray) -> dict:
448
+ """
449
+ DCT frequency domain analysis.
450
+ Returns histogram of frequency magnitudes β€” AI images have characteristic
451
+ patterns in high-frequency bands.
452
+ """
453
+ try:
454
+ from scipy.fftpack import dct
455
+ gray = arr.mean(axis=2).astype(np.float32)
456
+ # Sample a center patch
457
+ h, w = gray.shape
458
+ patch_size = min(256, h, w)
459
+ py, px = (h - patch_size) // 2, (w - patch_size) // 2
460
+ patch = gray[py:py+patch_size, px:px+patch_size]
461
+
462
+ # 2D DCT
463
+ dct_2d = dct(dct(patch, axis=0, norm='ortho'), axis=1, norm='ortho')
464
+ mag = np.abs(dct_2d).flatten()
465
+
466
+ # Log-scale histogram with 32 bins
467
+ mag_log = np.log1p(mag)
468
+ hist, _ = np.histogram(mag_log, bins=32)
469
+ hist_norm = (hist / (hist.max() + 1e-6)).tolist()
470
+
471
+ return {"bins": list(range(32)), "magnitudes": hist_norm}
472
+ except Exception as e:
473
+ logger.debug(f"[FreqAnalysis] computation failed: {e}")
474
+ return {"bins": list(range(32)), "magnitudes": [0.5] * 32}
frontend/index.html CHANGED
@@ -63,52 +63,52 @@ input,textarea{font-family:inherit;cursor:none;user-select:text;-webkit-user-sel
63
  /* BG */
64
  .bg-layer{position:fixed;inset:0;z-index:0;pointer-events:none;
65
  background:
66
- radial-gradient(ellipse 70% 55% at 5% 0%,rgba(255,10,108,0.14) 0%,transparent 60%),
67
- radial-gradient(ellipse 60% 50% at 95% 100%,rgba(124,58,237,0.13) 0%,transparent 55%),
68
- radial-gradient(ellipse 50% 50% at 50% 50%,rgba(255,111,168,0.07) 0%,transparent 65%),
69
- repeating-linear-gradient(0deg,rgba(255,10,108,0.05) 0px,rgba(255,10,108,0.05) 1px,transparent 1px,transparent 24px),
70
- repeating-linear-gradient(90deg,rgba(124,58,237,0.05) 0px,rgba(124,58,237,0.05) 1px,transparent 1px,transparent 24px),
71
- linear-gradient(135deg,rgba(255,240,248,0.97) 0%,rgba(248,235,255,0.96) 50%,rgba(255,243,250,0.97) 100%);}
72
  .grain{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:0.03;
73
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)'/%3E%3C/svg%3E");
74
  background-size:200px;}
75
  .blob{position:fixed;border-radius:50%;filter:blur(90px);pointer-events:none;z-index:0;animation:blobFloat 18s ease-in-out infinite;}
76
- .b1{width:700px;height:700px;top:-200px;left:-200px;background:radial-gradient(circle,rgba(255,10,108,0.22),transparent 65%);}
77
- .b2{width:500px;height:500px;bottom:-100px;right:-100px;background:radial-gradient(circle,rgba(124,58,237,0.20),transparent 65%);animation-delay:-9s;}
78
- .b3{width:400px;height:400px;top:45%;left:40%;background:radial-gradient(circle,rgba(255,111,168,0.16),transparent 65%);animation-delay:-15s;}
79
  @keyframes blobFloat{0%,100%{transform:translate(0,0)scale(1)}33%{transform:translate(50px,-40px)scale(1.06)}66%{transform:translate(-30px,30px)scale(0.94)}}
80
 
81
  /* CURSOR */
82
  #cur{position:fixed;width:10px;height:10px;background:var(--blue);border-radius:50%;pointer-events:none;z-index:99999;transform:translate(-50%,-50%);transition:width .2s var(--ease),height .2s var(--ease),opacity .2s;mix-blend-mode:normal;}
83
- #cur-ring{position:fixed;width:30px;height:30px;border:1.5px solid rgba(124,58,237,0.65);border-radius:50%;pointer-events:none;z-index:99998;transform:translate(-50%,-50%);transition:all .1s ease;}
84
  .h-active #cur{width:22px;height:22px;}
85
- .c-active #cur{width:14px;height:14px;background:rgba(124,58,237,0.8);}
86
 
87
  /* LAYOUT */
88
  .app{position:relative;z-index:2;min-height:100vh;}
89
  .container{max-width:1280px;margin:0 auto;padding:0 40px;}
90
 
91
  /* HEADER */
92
- header{position:sticky;top:0;z-index:100;backdrop-filter:blur(24px) saturate(180%);-webkit-backdrop-filter:blur(24px) saturate(180%);background:rgba(255,240,248,0.60);border-bottom:1px solid rgba(255,255,255,0.35);box-shadow:0 1px 24px rgba(255,10,108,0.12),0 1px 0 rgba(124,58,237,0.08);}
93
  .header-inner{display:flex;align-items:center;justify-content:space-between;height:64px;}
94
  .logo{display:flex;align-items:center;gap:10px;font-family:'Playfair Display',serif;font-weight:700;font-size:20px;letter-spacing:0.05em;color:var(--text-1);}
95
- .logo-icon{width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,#FF0A6C,#7C3AED);display:flex;align-items:center;justify-content:center;font-size:16px;box-shadow:0 4px 14px rgba(255,10,108,0.45);}
96
  .logo-sub{font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:400;color:var(--text-4);letter-spacing:0.15em;display:block;margin-top:1px;}
97
  .header-center{display:flex;gap:4px;}
98
  .nav-btn{padding:7px 16px;border-radius:100px;font-size:13px;font-weight:500;color:var(--text-3);letter-spacing:0.01em;transition:all 0.2s;cursor:none;border:1px solid transparent;}
99
  .nav-btn:hover,.nav-btn.active{background:rgba(255,255,255,0.2);border-color:rgba(255,255,255,0.35);color:var(--text-1);box-shadow:var(--shadow-sm);}
100
  .nav-btn.active{color:var(--blue);}
101
  .header-right{display:flex;align-items:center;gap:10px;}
102
- .status-badge{display:flex;align-items:center;gap:6px;padding:6px 12px;border-radius:100px;background:rgba(255,255,255,0.30);border:1px solid rgba(255,10,108,0.25);box-shadow:var(--shadow-sm);font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-3);}
103
  .dot-live{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:liveGlow 2s ease-in-out infinite;}
104
  @keyframes liveGlow{0%,100%{opacity:1}50%{opacity:0.5}}
105
 
106
  /* HERO */
107
  .hero{padding:40px 0 20px;}
108
- .hero-title{font-family:'Playfair Display',serif;font-size:42px;font-weight:900;letter-spacing:-0.02em;line-height:1.1;background:linear-gradient(135deg,#FF0A6C 0%,#7C3AED 55%,#BF5AF2 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:10px;filter:drop-shadow(0 2px 12px rgba(255,10,108,0.18));}
109
  .hero-sub{font-size:15px;color:var(--text-3);font-weight:400;line-height:1.5;}
110
  .hero-badges{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px;}
111
- .hero-badge{display:flex;align-items:center;gap:5px;padding:5px 12px;border-radius:100px;font-size:11px;font-weight:600;letter-spacing:0.04em;background:rgba(255,255,255,0.30);border:1px solid rgba(255,10,108,0.18);box-shadow:var(--shadow-sm);color:var(--text-1);animation:badgeFade 0.5s var(--ease) backwards;}
112
  .hero-badge:nth-child(1){animation-delay:.05s}
113
  .hero-badge:nth-child(2){animation-delay:.1s}
114
  .hero-badge:nth-child(3){animation-delay:.15s}
@@ -261,6 +261,8 @@ header{position:sticky;top:0;z-index:100;backdrop-filter:blur(24px) saturate(180
261
  .mod-card.running .mod-status{background:var(--amber);box-shadow:0 0 8px var(--amber);animation:liveGlow 0.8s ease-in-out infinite;}
262
  .mod-card.disabled-mod{opacity:0.4;filter:grayscale(0.6);}
263
  .mod-card.disabled-mod .mod-status{background:rgba(0,0,0,0.1);}
 
 
264
  .mod-icon{font-size:18px;margin-bottom:6px;display:block;}
265
  .mod-name{font-size:10px;font-weight:700;color:var(--text-2);letter-spacing:0.04em;margin-bottom:2px;}
266
  .mod-desc{font-size:9px;color:var(--text-4);line-height:1.4;}
@@ -426,51 +428,6 @@ canvas{width:100%;display:block;}
426
  .fade-in:nth-child(1){animation-delay:.05s}
427
  .fade-in:nth-child(2){animation-delay:.1s}
428
  @keyframes fadeIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
429
- .mod-card.mod-flagged{border-color:rgba(220,38,38,0.35)!important;background:rgba(220,38,38,0.06)!important;}
430
- .mod-card.mod-flagged .mod-status{background:var(--red)!important;box-shadow:0 0 8px var(--red)!important;}
431
-
432
- /* AI SUMMARY CARD */
433
- .ai-summary-card{padding:16px 22px;border-top:1px solid rgba(255,255,255,0.2);display:none;}
434
- .ai-summary-card.visible{display:block;}
435
- .ai-summary-title{font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:700;color:var(--text-4);letter-spacing:0.15em;text-transform:uppercase;margin-bottom:8px;}
436
- .ai-summary-text{font-size:12px;color:var(--text-2);line-height:1.65;padding:12px 14px;border-radius:10px;background:rgba(255,255,255,0.22);border:1px solid rgba(255,255,255,0.4);}
437
-
438
- /* BATCH SECTION */
439
- .batch-drop-grid{display:flex;flex-direction:column;gap:10px;}
440
- .batch-file-row{display:flex;align-items:center;gap:12px;padding:12px 16px;border-radius:12px;background:rgba(255,255,255,0.22);border:1px solid rgba(255,255,255,0.4);}
441
- .batch-file-icon{font-size:20px;}
442
- .batch-file-info{flex:1;}
443
- .batch-file-name{font-size:13px;font-weight:600;color:var(--text-1);}
444
- .batch-file-size{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-4);}
445
- .batch-file-status{font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:700;}
446
- .batch-file-remove{width:24px;height:24px;border-radius:50%;background:rgba(220,38,38,0.08);border:1px solid rgba(220,38,38,0.2);color:var(--red);font-size:12px;display:flex;align-items:center;justify-content:center;cursor:none;transition:all 0.2s;}
447
- .batch-file-remove:hover{background:var(--red-pale);}
448
- .batch-result-card{padding:16px;border-radius:14px;background:rgba(255,255,255,0.22);border:1px solid rgba(255,255,255,0.4);display:flex;align-items:center;gap:14px;cursor:none;transition:all 0.2s;}
449
- .batch-result-card:hover{border-color:rgba(79,172,254,0.4);transform:translateX(3px);}
450
- .batch-verdict-badge{padding:5px 12px;border-radius:100px;font-size:11px;font-weight:700;letter-spacing:0.06em;white-space:nowrap;}
451
- .batch-summary-bar{display:flex;gap:8px;padding:12px 16px;border-radius:12px;background:rgba(255,255,255,0.22);border:1px solid rgba(255,255,255,0.3);margin-bottom:16px;}
452
- .batch-stat{flex:1;text-align:center;}
453
- .batch-stat-num{font-family:'JetBrains Mono',monospace;font-size:22px;font-weight:700;}
454
- .batch-stat-lbl{font-size:10px;color:var(--text-4);letter-spacing:0.08em;}
455
-
456
- /* HISTORY SECTION */
457
- .history-filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;}
458
- .hist-filter-btn{padding:5px 14px;border-radius:100px;font-size:11px;font-weight:600;background:rgba(255,255,255,0.22);border:1px solid rgba(255,255,255,0.4);color:var(--text-3);cursor:none;transition:all 0.2s;}
459
- .hist-filter-btn:hover,.hist-filter-btn.active{background:var(--blue-pale);border-color:rgba(79,172,254,0.5);color:var(--blue);}
460
- .history-grid{display:flex;flex-direction:column;gap:10px;}
461
- .history-card{display:flex;align-items:center;gap:14px;padding:14px 18px;border-radius:14px;background:rgba(255,255,255,0.18);border:1px solid rgba(255,255,255,0.4);cursor:none;transition:all 0.25s;}
462
- .history-card:hover{background:rgba(255,255,255,0.28);border-color:rgba(79,172,254,0.35);transform:translateX(4px);}
463
- .history-icon{font-size:22px;flex-shrink:0;}
464
- .history-info{flex:1;min-width:0;}
465
- .history-name{font-size:13px;font-weight:700;color:var(--text-1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
466
- .history-meta{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-4);margin-top:2px;}
467
- .history-verdict{padding:4px 12px;border-radius:100px;font-size:11px;font-weight:700;letter-spacing:0.06em;white-space:nowrap;flex-shrink:0;}
468
- .history-risk{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;min-width:48px;text-align:right;}
469
- .history-empty{text-align:center;padding:48px 24px;color:var(--text-4);font-family:'JetBrains Mono',monospace;font-size:12px;}
470
- .hist-stats-row{display:flex;gap:12px;margin-bottom:16px;}
471
- .hist-stat-pill{flex:1;padding:12px;border-radius:12px;background:rgba(255,255,255,0.22);border:1px solid rgba(255,255,255,0.35);text-align:center;}
472
- .hist-stat-pill-num{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:700;color:var(--text-1);}
473
- .hist-stat-pill-lbl{font-size:10px;color:var(--text-4);letter-spacing:0.08em;margin-top:2px;}
474
  </style>
475
  </head>
476
  <body>
@@ -498,17 +455,15 @@ canvas{width:100%;display:block;}
498
  <div class="header-inner">
499
  <div class="logo">
500
  <div class="logo-icon">πŸ”¬</div>
501
- <div>VERIDEX <span class="logo-sub">FORENSIC AI PLATFORM v4.1</span></div>
502
  </div>
503
  <nav class="header-center">
504
  <button class="nav-btn active" onclick="showSection('analyze')">Analyze</button>
505
- <button class="nav-btn" onclick="showSection('batch')">⚑ Batch</button>
506
  <button class="nav-btn" onclick="showSection('modules')">46 Modules</button>
507
- <button class="nav-btn" onclick="showSection('history')">πŸ•˜ History</button>
508
  <button class="nav-btn" onclick="showSection('custody')">Chain of Custody</button>
509
  </nav>
510
  <div class="header-right">
511
- <div class="status-badge" id="healthBadge"><div class="dot-live"></div><span id="activeModCount">46</span>/46 Active</div>
512
  </div>
513
  </div>
514
  </div>
@@ -649,16 +604,6 @@ canvas{width:100%;display:block;}
649
  <button class="btn-report" id="btnReport" disabled onclick="openReport()">πŸ“„ GENERATE COURT REPORT</button>
650
  </div>
651
 
652
- <!-- AI SUMMARY CARD -->
653
- <div class="card fade-in" id="aiSummaryCard" style="display:none;">
654
- <div class="card-header"><div class="card-title"><div class="card-title-icon" style="background:rgba(124,58,237,0.08)">🧠</div>AI Forensic Summary</div></div>
655
- <div class="ai-summary-card visible" id="aiSummaryWrap">
656
- <div class="ai-summary-title">πŸ“‹ Analysis Conclusion</div>
657
- <div class="ai-summary-text" id="aiSummaryText">Awaiting analysis...</div>
658
- </div>
659
- <div style="padding:0 22px 16px;display:flex;flex-direction:column;gap:6px;" id="keyFindingsList"></div>
660
- </div>
661
-
662
  <!-- METADATA -->
663
  <div class="card fade-in">
664
  <div class="card-header"><div class="card-title"><div class="card-title-icon" style="background:rgba(13,148,136,0.08)">πŸ—‚οΈ</div>File Metadata</div></div>
@@ -677,96 +622,6 @@ canvas{width:100%;display:block;}
677
  </div>
678
  </div>
679
 
680
- <!-- BATCH SECTION β€” NEW -->
681
- <div id="sec-batch" style="display:none;padding-bottom:40px">
682
- <div class="sec-lbl">Batch Forensic Analysis</div>
683
- <div class="main-grid">
684
- <div class="left-col">
685
- <div class="card">
686
- <div class="card-header">
687
- <div class="card-title"><div class="card-title-icon" style="background:var(--blue-pale)">⚑</div>Upload Multiple Files</div>
688
- <div class="status-badge" style="font-size:11px"><span id="batchCount">0</span>/10 files</div>
689
- </div>
690
- <div class="card-pad">
691
- <div class="upload-zone" id="batchZone" onclick="document.getElementById('batchInput').click()">
692
- <input type="file" id="batchInput" multiple accept="image/*,video/*,audio/*,.pdf" style="display:none">
693
- <div class="upload-icon-wrap"><span>πŸ“‚</span></div>
694
- <div class="upload-title">Drop up to 10 files</div>
695
- <div class="upload-sub">All formats Β· Analyzed in parallel Β· One report per file</div>
696
- <div class="upload-formats">
697
- <span class="fmt-tag">Images</span><span class="fmt-tag">Videos</span>
698
- <span class="fmt-tag">Audio</span><span class="fmt-tag">PDFs</span>
699
- </div>
700
- </div>
701
- </div>
702
- <div style="padding:0 22px 16px" id="batchFileList"></div>
703
- <div style="padding:0 22px 16px">
704
- <button class="btn-analyze" id="btnBatch" onclick="startBatch()" style="display:none">
705
- <span class="btn-text">⚑ ANALYZE ALL FILES</span>
706
- <div class="btn-spinner"></div>
707
- </button>
708
- </div>
709
- </div>
710
- </div>
711
- <div class="right-col">
712
- <div class="card" id="batchResultsCard" style="display:none;">
713
- <div class="card-header"><div class="card-title"><div class="card-title-icon" style="background:var(--green-pale)">πŸ“Š</div>Batch Results</div></div>
714
- <div style="padding:16px">
715
- <div class="batch-summary-bar" id="batchSummaryBar"></div>
716
- <div class="batch-drop-grid" id="batchResultsList"></div>
717
- </div>
718
- </div>
719
- <div class="card">
720
- <div class="card-header"><div class="card-title"><div class="card-title-icon" style="background:rgba(245,158,11,0.08)">ℹ️</div>Batch Info</div></div>
721
- <div style="padding:16px;font-size:12px;color:var(--text-3);line-height:1.7">
722
- <b style="color:var(--text-1)">How Batch Works:</b><br>
723
- β€’ Upload up to 10 files at once<br>
724
- β€’ Each file runs the full 46-module pipeline<br>
725
- β€’ Results appear as each file completes<br>
726
- β€’ Each case gets its own PDF report<br>
727
- β€’ All results saved to History
728
- </div>
729
- </div>
730
- </div>
731
- </div>
732
- </div>
733
-
734
- <!-- HISTORY SECTION β€” NEW -->
735
- <div id="sec-history" style="display:none;padding-bottom:40px">
736
- <div class="sec-lbl">Case History</div>
737
- <div class="card">
738
- <div class="card-header">
739
- <div class="card-title"><div class="card-title-icon" style="background:rgba(99,102,241,0.08)">πŸ•˜</div>Past Analysis Cases</div>
740
- <div style="display:flex;gap:8px">
741
- <button class="mod-ctrl-btn" onclick="loadHistory()">πŸ”„ Refresh</button>
742
- <button class="mod-ctrl-btn" onclick="clearHistory()" style="color:var(--red)">πŸ—‘οΈ Clear All</button>
743
- </div>
744
- </div>
745
- <div style="padding:16px 22px">
746
- <!-- Stats row -->
747
- <div class="hist-stats-row" id="histStatsRow">
748
- <div class="hist-stat-pill"><div class="hist-stat-pill-num" id="histTotal">0</div><div class="hist-stat-pill-lbl">TOTAL CASES</div></div>
749
- <div class="hist-stat-pill"><div class="hist-stat-pill-num" style="color:var(--red)" id="histFakes">0</div><div class="hist-stat-pill-lbl">FAKES</div></div>
750
- <div class="hist-stat-pill"><div class="hist-stat-pill-num" style="color:var(--green)" id="histAuthentic">0</div><div class="hist-stat-pill-lbl">AUTHENTIC</div></div>
751
- <div class="hist-stat-pill"><div class="hist-stat-pill-num" style="color:var(--synth)" id="histSynth">0</div><div class="hist-stat-pill-lbl">SYNTHETIC</div></div>
752
- </div>
753
- <!-- Filters -->
754
- <div class="history-filters">
755
- <button class="hist-filter-btn active" onclick="filterHistory('all',this)">All</button>
756
- <button class="hist-filter-btn" onclick="filterHistory('FAKE',this)">⚠️ Fake</button>
757
- <button class="hist-filter-btn" onclick="filterHistory('AUTHENTIC',this)">βœ… Authentic</button>
758
- <button class="hist-filter-btn" onclick="filterHistory('SYNTHETIC',this)">🧬 Synthetic</button>
759
- <button class="hist-filter-btn" onclick="filterHistory('image',this)">πŸ–ΌοΈ Images</button>
760
- <button class="hist-filter-btn" onclick="filterHistory('video',this)">πŸŽ₯ Videos</button>
761
- <button class="hist-filter-btn" onclick="filterHistory('audio',this)">🎡 Audio</button>
762
- </div>
763
- <div class="history-grid" id="historyGrid">
764
- <div class="history-empty">πŸ“­ No cases yet β€” run an analysis to see history here</div>
765
- </div>
766
- </div>
767
- </div>
768
- </div>
769
-
770
  <!-- MODULES SECTION -->
771
  <div id="sec-modules" style="display:none;padding-bottom:40px">
772
  <div class="sec-lbl">All 46 Detection Modules</div>
@@ -1298,14 +1153,88 @@ function resetResults(){
1298
  function resetFrameBars(){Array.from({length:80},(_,i)=>{const fb=document.getElementById('fb'+i);if(fb){fb.className='fb idle';fb.style.height=(20+Math.random()*15)+'px';}});}
1299
 
1300
  // ═══════════════════════════════════════════════════
1301
- // BACKEND API β€” same-origin (FastAPI serves frontend)
1302
- // Run: uvicorn main:app --reload --port 8000
1303
- // Then open: http://localhost:8000
1304
  // ═══════════════════════════════════════════════════
1305
- const API_BASE = '';
 
 
 
 
 
 
 
 
 
 
 
1306
 
1307
  // ═══════════════════════════════════════════════════
1308
- // ANALYSIS β€” real backend only, no fallback/simulation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1309
  // ═══════════════════════════════════════════════════
1310
  async function startAnalysis(){
1311
  if(!state.file){toast('⚠️ Please upload a file first',2500);return;}
@@ -1335,87 +1264,89 @@ async function startAnalysis(){
1335
  if(pct>15 && msgIdx<msgs.length){toast(msgs[msgIdx++],1200);}
1336
  },400);
1337
 
 
 
 
1338
  try {
1339
  const formData=new FormData();
1340
  formData.append('file', state.file);
1341
  const enabledIds=active.map(m=>m.id).join(',');
1342
  formData.append('modules', enabledIds);
1343
 
1344
- const response = await fetch(`${API_BASE}/analyze`, {method:'POST', body:formData});
1345
- clearInterval(interval);
1346
 
 
1347
  if(!response.ok){
1348
  let errMsg='Analysis failed';
1349
  try{const err=await response.json();errMsg=err.detail||errMsg;}catch(e){}
1350
  throw new Error(`Server ${response.status}: ${errMsg}`);
1351
  }
1352
-
1353
- const data = await response.json();
1354
- progBar.style.width='100%';
1355
-
1356
- state.analysisId = data.case_id;
1357
- state.timestamp = data.timestamp;
1358
- state.apiData = data;
1359
-
1360
- if(data.sha256){
1361
- state.hash = data.sha256;
1362
- state.md5 = data.md5||'';
1363
- const md5Line = data.md5 ? `<br><strong>MD5:</strong><br>${data.md5}` : '';
1364
- document.getElementById('hashDisplay').innerHTML=`<strong>SHA-256:</strong><br>${data.sha256}${md5Line}`;
1365
- document.getElementById('hashStatus').textContent='βœ…';
1366
- document.getElementById('hashStatusTxt').textContent='Server-verified integrity';
1367
- document.getElementById('chash1').textContent=data.sha256.slice(0,16)+'...';
1368
- markCustodyStep(1,data.sha256.slice(0,16)+'...');
1369
- }
1370
-
1371
- // Map ALL backend module scores to state (0.0-1.0 β†’ 0-100%)
1372
- const scores=data.scores||{};
1373
- Object.entries(scores).forEach(([k,v])=>{
1374
- const mid=parseInt(k);
1375
- if(!isNaN(mid)) state.moduleResults[mid]=Math.round(parseFloat(v)*100);
1376
- });
1377
-
1378
- // Animate ALL 46 module cards with real scores
1379
- active.forEach((m,i)=>{
1380
- setTimeout(()=>{
1381
- const el=document.getElementById('mod'+m.id);
1382
- const fp=document.getElementById('fp'+m.id);
1383
- const score=state.moduleResults[m.id]??70;
1384
- if(el) el.className='mod-card on'+(score<40?' mod-flagged':'');
1385
- if(fp) fp.className='feat-pip on';
1386
- },i*40);
1387
- });
1388
-
1389
- document.getElementById('scanBeam').classList.remove('active');
1390
- btn.classList.remove('loading');
1391
-
1392
- const verdict = data.verdict||'UNKNOWN';
1393
- const isFake = verdict==='FAKE'||verdict==='DEEPFAKE';
1394
- const isSynth = data.is_synth||verdict==='SYNTHETIC';
1395
- const score = Math.round(data.confidence*100);
1396
-
1397
- finishAnalysisFromAPI(data, score, isFake, isSynth);
1398
 
1399
  } catch(err) {
1400
  clearInterval(interval);
1401
- btn.classList.remove('loading');
1402
- document.getElementById('scanBeam').classList.remove('active');
1403
- progBar.style.width='0%'; progWrap.classList.remove('active');
1404
- active.forEach(m=>{const el=document.getElementById('mod'+m.id);if(el)el.className='mod-card';});
1405
-
1406
- const isNetwork=err.message.toLowerCase().includes('fetch')||err.message.toLowerCase().includes('network')||err.message.toLowerCase().includes('failed');
1407
  if(isNetwork){
1408
- toast('❌ Cannot reach backend β€” make sure uvicorn is running on port 8000',7000);
 
 
 
1409
  } else {
1410
- toast('❌ '+err.message, 5000);
 
 
 
 
 
 
1411
  }
1412
- console.error('[VERIDEX] Analysis error:',err);
1413
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1414
  }
1415
 
1416
- // ═══════════════════════════════════════════════════
1417
- // FINISH β€” render real backend results
1418
- // ═══════════════════════════════════════════════════
1419
  function finishAnalysisFromAPI(data, score, isFake, isSynth){
1420
  state.finalScore=score; state.isFake=isFake; state.isSynth=isSynth; state.analyzed=true;
1421
 
@@ -1432,7 +1363,7 @@ function finishAnalysisFromAPI(data, score, isFake, isSynth){
1432
  else if(isSynth){chip.className='verdict-chip synth';chip.textContent='🧬 SYNTHETIC DETECTED';}
1433
  else{chip.className='verdict-chip authentic';chip.textContent='βœ… AUTHENTIC MEDIA';}
1434
 
1435
- // 8 highlighted sub-score bars
1436
  const scores=data.scores||{};
1437
  const subMap=[
1438
  ['6','RGB Forensic'],['7','Frequency'],['8','Noise Residual'],['9','rPPG Signal'],
@@ -1452,12 +1383,24 @@ function finishAnalysisFromAPI(data, score, isFake, isSynth){
1452
  },i*150);
1453
  });
1454
 
1455
- // Synth ID card
 
 
 
 
 
 
 
 
 
 
 
 
1456
  if(isSynth && data.synth_data && Object.keys(data.synth_data).length>0){
1457
  const sd=data.synth_data;
1458
  state.synthData={
1459
  model:sd.generator||'Unknown',generator:sd.provider||'Unknown',
1460
- confidence:Math.round((sd.confidence||0)*100),
1461
  faceMatch:'GENERATED',watermark:sd.watermark_detected?'DETECTED':'NOT FOUND',
1462
  signalType:sd.signal_type||'Unknown',
1463
  };
@@ -1470,19 +1413,17 @@ function finishAnalysisFromAPI(data, score, isFake, isSynth){
1470
  setTimeout(()=>{document.getElementById('synthConfFill').style.width=state.synthData.confidence+'%';},300);
1471
  }
1472
 
1473
- // Key findings
1474
  if(data.key_findings && data.key_findings.length>0){
1475
  const meta=document.getElementById('metaPanel');
1476
- const html=data.key_findings.map(f=>`<div class="rep-field" style="grid-column:1/-1"><div class="rep-f-lbl">πŸ” FINDING</div><div class="rep-f-val" style="font-size:11px;color:var(--text-2)">${f}</div></div>`).join('');
1477
- if(meta) meta.innerHTML=(meta.innerHTML||'')+html;
1478
  }
1479
 
1480
- // Video panel
1481
- const vfg=document.getElementById('vForensicGrid');
1482
- if(vfg){
1483
- const vstf=vfg.querySelectorAll('.vstf');
1484
- const results=isFake?['⚠️ ANOMALY','⚠️ MISMATCH','⚠️ BROKEN','⚠️ WARPED','⚠️ ABSENT','⚠️ LOW']:['βœ… STABLE','βœ… SYNCED','βœ… COHERENT','βœ… NATURAL','βœ… DETECTED','βœ… NORMAL'];
1485
- vstf.forEach((c,i)=>{c.textContent=results[i];c.style.color=isFake?'var(--red)':'var(--green)';});
1486
  }
1487
 
1488
  animateFrameBars(isFake);
@@ -1492,48 +1433,86 @@ function finishAnalysisFromAPI(data, score, isFake, isSynth){
1492
  document.getElementById('btnReport').disabled=false;
1493
  document.getElementById('progWrap').classList.remove('active');
1494
 
1495
- // AI Summary card
1496
- showAISummary(data);
1497
-
1498
- // ── Server heatmap & chart images (from backend /visuals/) ──
1499
- if(data.heatmap_url){
1500
- const heatmapCanvas = document.getElementById('heatmapCanvas');
1501
- if(heatmapCanvas){
1502
- const img = new Image();
1503
- img.crossOrigin = 'anonymous';
1504
- img.onload = ()=>{
1505
- const ctx = heatmapCanvas.getContext('2d');
1506
- const w = heatmapCanvas.parentElement.offsetWidth||160;
1507
- heatmapCanvas.width = w*2;
1508
- heatmapCanvas.height = 180;
1509
- heatmapCanvas.style.height = '90px';
1510
- ctx.drawImage(img, 0, 0, heatmapCanvas.width, heatmapCanvas.height);
1511
- };
1512
- img.src = (window.API_BASE||'http://localhost:8000') + data.heatmap_url;
1513
- }
1514
- }
1515
- if(data.chart_url){
1516
- // Inject score chart image into AI summary card if available
1517
- const aiCard = document.getElementById('aiSummaryCard');
1518
- if(aiCard && !document.getElementById('serverChartImg')){
1519
- const imgWrap = document.createElement('div');
1520
- imgWrap.style.cssText='margin-top:12px;border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,0.3)';
1521
- const img = document.createElement('img');
1522
- img.id = 'serverChartImg';
1523
- img.src = (window.API_BASE||'http://localhost:8000') + data.chart_url;
1524
- img.style.cssText = 'width:100%;display:block;border-radius:12px';
1525
- img.alt = 'Module Score Chart';
1526
- imgWrap.appendChild(img);
1527
- aiCard.appendChild(imgWrap);
1528
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1529
  }
1530
 
1531
- // Refresh history in background
1532
- setTimeout(loadHistory, 500);
 
 
 
 
 
 
 
 
 
 
 
1533
 
1534
- toast(isFake?'⚠️ DEEPFAKE DETECTED β€” report ready!':isSynth?'🧬 Synthetic AI media detected!':'βœ… Authentic media confirmed',4000);
1535
  }
1536
 
 
 
 
 
 
 
 
 
 
 
1537
 
1538
  function animateFrameBars(isFake){
1539
  Array.from({length:80},(_,i)=>setTimeout(()=>{
@@ -1626,18 +1605,9 @@ function updateCustodyLog(){
1626
  // NAVIGATION
1627
  // ═══════════════════════════════════════════════════
1628
  function showSection(name){
1629
- ['analyze','batch','modules','history','custody'].forEach(s=>{
1630
- const el=document.getElementById('sec-'+s);
1631
- if(el) el.style.display=s===name?'block':'none';
1632
- });
1633
- document.querySelectorAll('.nav-btn').forEach(b=>{
1634
- b.classList.toggle('active',
1635
- b.textContent.toLowerCase().includes(name) ||
1636
- (name==='analyze' && b.textContent.toLowerCase().includes('analyze'))
1637
- );
1638
- });
1639
- if(name==='custody') updateCustodyLog();
1640
- if(name==='history') loadHistory();
1641
  window.scrollTo({top:0,behavior:'smooth'});
1642
  }
1643
 
@@ -1667,7 +1637,7 @@ function openReport(){
1667
  <div class="rep-field"><div class="rep-f-lbl">Active Modules</div><div class="rep-f-val">${MODULES.length-state.disabledModules.size}/46</div></div>
1668
  </div></div>
1669
  <div class="report-section"><div class="rep-section-title">πŸ” Cryptographic Integrity</div>
1670
- <div class="rep-hash"><strong>SHA-256:</strong><br>${state.hash||'N/A'}${state.md5?`<br><br><strong>MD5:</strong><br>${state.md5}`:''}<br><br><strong>CHAIN OF CUSTODY:</strong> File β†’ Hash β†’ Analysis β†’ Verification β†’ Report<br><strong>TAMPER STATUS:</strong> βœ… UNMODIFIED</div>
1671
  </div>
1672
  <div class="report-section"><div class="rep-section-title">βš–οΈ Forensic Verdict</div>
1673
  <div style="display:flex;align-items:center;justify-content:space-between;padding:16px;border-radius:14px;background:${state.isFake?'var(--red-pale)':state.isSynth?'var(--synth-pale)':'var(--green-pale)'};border:1px solid ${state.isFake?'rgba(220,38,38,0.2)':state.isSynth?'rgba(139,92,246,0.2)':'rgba(22,163,74,0.2)'}">
@@ -1740,293 +1710,6 @@ function formatSize(b){if(b>1024*1024)return(b/1024/1024).toFixed(1)+' MB';if(b>
1740
  function shortHash(){return Array.from({length:16},()=>'0123456789abcdef'[Math.floor(Math.random()*16)]).join('');}
1741
  function toast(msg,dur=2500){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),dur);}
1742
 
1743
- // ═══════════════════════════════════════════════════
1744
- // AI SUMMARY DISPLAY
1745
- // ═══════════════════════════════════════════════════
1746
- function showAISummary(data){
1747
- const card=document.getElementById('aiSummaryCard');
1748
- const text=document.getElementById('aiSummaryText');
1749
- const list=document.getElementById('keyFindingsList');
1750
- if(!card||!text) return;
1751
- card.style.display='block';
1752
- text.textContent=data.ai_summary||'Analysis complete.';
1753
- list.innerHTML=(data.key_findings||[]).slice(0,5).map((f,i)=>`
1754
- <div style="display:flex;align-items:flex-start;gap:8px;padding:8px 12px;border-radius:8px;background:rgba(255,255,255,0.18);border:1px solid rgba(255,255,255,0.3)">
1755
- <span style="font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:700;color:var(--blue);flex-shrink:0">${String(i+1).padStart(2,'0')}.</span>
1756
- <span style="font-size:11px;color:var(--text-2);line-height:1.5">${f}</span>
1757
- </div>`).join('');
1758
- }
1759
-
1760
- // ═══════════════════════════════════════════════════
1761
- // BATCH ANALYSIS β€” NEW
1762
- // ═══════════════════════════════════════════════════
1763
- const _batchFiles=[];
1764
-
1765
- const batchZone=document.getElementById('batchZone');
1766
- const batchInput=document.getElementById('batchInput');
1767
- if(batchZone){
1768
- batchZone.addEventListener('dragover',e=>{e.preventDefault();batchZone.classList.add('drag-over');});
1769
- batchZone.addEventListener('dragleave',()=>batchZone.classList.remove('drag-over'));
1770
- batchZone.addEventListener('drop',e=>{e.preventDefault();batchZone.classList.remove('drag-over');Array.from(e.dataTransfer.files).forEach(addBatchFile);});
1771
- }
1772
- if(batchInput) batchInput.addEventListener('change',e=>Array.from(e.target.files).forEach(addBatchFile));
1773
-
1774
- function addBatchFile(file){
1775
- if(_batchFiles.length>=10){toast('⚠️ Max 10 files per batch',2500);return;}
1776
- if(_batchFiles.some(f=>f.name===file.name&&f.size===file.size)){toast('⚠️ Duplicate file skipped',2000);return;}
1777
- _batchFiles.push(file);
1778
- renderBatchFileList();
1779
- }
1780
-
1781
- function removeBatchFile(idx){
1782
- _batchFiles.splice(idx,1);
1783
- renderBatchFileList();
1784
- }
1785
-
1786
- function renderBatchFileList(){
1787
- const el=document.getElementById('batchFileList');
1788
- const cnt=document.getElementById('batchCount');
1789
- const btn=document.getElementById('btnBatch');
1790
- if(!el) return;
1791
- if(cnt) cnt.textContent=_batchFiles.length;
1792
- if(btn) btn.style.display=_batchFiles.length>0?'block':'none';
1793
- if(_batchFiles.length===0){el.innerHTML='';return;}
1794
- const typeIcon=t=>t.startsWith('image')?'πŸ–ΌοΈ':t.startsWith('video')?'πŸŽ₯':t.startsWith('audio')?'🎡':'πŸ“„';
1795
- el.innerHTML='<div class="batch-drop-grid">'+_batchFiles.map((f,i)=>`
1796
- <div class="batch-file-row">
1797
- <span class="batch-file-icon">${typeIcon(f.type||'')}</span>
1798
- <div class="batch-file-info">
1799
- <div class="batch-file-name">${f.name}</div>
1800
- <div class="batch-file-size">${formatSize(f.size)} Β· ${f.type||'Unknown'}</div>
1801
- </div>
1802
- <div class="batch-file-status" id="bfstatus${i}" style="color:var(--text-4)">⏳ QUEUED</div>
1803
- <button class="batch-file-remove" onclick="removeBatchFile(${i})">βœ•</button>
1804
- </div>`).join('')+'</div>';
1805
- }
1806
-
1807
- async function startBatch(){
1808
- if(_batchFiles.length===0){toast('⚠️ Add files first',2000);return;}
1809
- const btn=document.getElementById('btnBatch');
1810
- btn.classList.add('loading');
1811
- toast('⚑ Sending '+_batchFiles.length+' files for analysis...',3000);
1812
-
1813
- // Update statuses to running
1814
- _batchFiles.forEach((_,i)=>{
1815
- const s=document.getElementById('bfstatus'+i);
1816
- if(s){s.textContent='πŸ”„ RUNNING';s.style.color='var(--amber)';}
1817
- });
1818
-
1819
- try{
1820
- const fd=new FormData();
1821
- _batchFiles.forEach(f=>fd.append('files',f));
1822
- fd.append('modules','all');
1823
-
1824
- const resp=await fetch(`${API_BASE}/batch`,{method:'POST',body:fd});
1825
- if(!resp.ok){
1826
- const err=await resp.json().catch(()=>({}));
1827
- throw new Error(err.detail||'Batch failed ('+resp.status+')');
1828
- }
1829
- const data=await resp.json();
1830
- btn.classList.remove('loading');
1831
-
1832
- // Update statuses
1833
- (data.results||[]).forEach((r,i)=>{
1834
- const s=document.getElementById('bfstatus'+i);
1835
- if(!s)return;
1836
- if(r.error){s.textContent='❌ ERROR';s.style.color='var(--red)';return;}
1837
- const v=r.verdict||'UNKNOWN';
1838
- const isFake=v==='FAKE'||v==='DEEPFAKE';
1839
- const isSynth=v==='SYNTHETIC';
1840
- s.textContent=isFake?'⚠️ FAKE':isSynth?'🧬 SYNTH':'βœ… AUTH';
1841
- s.style.color=isFake?'var(--red)':isSynth?'var(--synth)':'var(--green)';
1842
- });
1843
-
1844
- renderBatchResults(data);
1845
- loadHistory(); // refresh history
1846
- toast(`βœ… Batch done: ${data.flagged} flagged / ${data.total} total`,4000);
1847
- }catch(e){
1848
- btn.classList.remove('loading');
1849
- toast('❌ Batch error: '+e.message,5000);
1850
- _batchFiles.forEach((_,i)=>{
1851
- const s=document.getElementById('bfstatus'+i);
1852
- if(s){s.textContent='❌ ERROR';s.style.color='var(--red)';}
1853
- });
1854
- }
1855
- }
1856
-
1857
- function renderBatchResults(data){
1858
- const card=document.getElementById('batchResultsCard');
1859
- const sumBar=document.getElementById('batchSummaryBar');
1860
- const list=document.getElementById('batchResultsList');
1861
- if(!card) return;
1862
- card.style.display='block';
1863
-
1864
- if(sumBar){
1865
- sumBar.innerHTML=[
1866
- {num:data.total,lbl:'TOTAL',c:'var(--text-1)'},
1867
- {num:data.flagged,lbl:'FLAGGED',c:'var(--red)'},
1868
- {num:data.authentic,lbl:'AUTHENTIC',c:'var(--green)'},
1869
- ].map(s=>`<div class="batch-stat"><div class="batch-stat-num" style="color:${s.c}">${s.num}</div><div class="batch-stat-lbl">${s.lbl}</div></div>`).join('');
1870
- }
1871
-
1872
- if(list){
1873
- list.innerHTML=(data.results||[]).map(r=>{
1874
- if(r.error) return `<div class="batch-result-card"><div style="font-size:13px;color:var(--red)">❌ ${r.file_name} β€” ${r.error}</div></div>`;
1875
- const v=r.verdict||'UNKNOWN';
1876
- const isFake=v==='FAKE'||v==='DEEPFAKE';
1877
- const isSynth=v==='SYNTHETIC';
1878
- const col=isFake?'var(--red)':isSynth?'var(--synth)':'var(--green)';
1879
- const bg=isFake?'var(--red-pale)':isSynth?'var(--synth-pale)':'var(--green-pale)';
1880
- const typeIcon=t=>t.startsWith('image')?'πŸ–ΌοΈ':t.startsWith('video')?'πŸŽ₯':t.startsWith('audio')?'🎡':'πŸ“„';
1881
- return `<div class="batch-result-card" onclick="openBatchDetail('${r.case_id}')">
1882
- <span style="font-size:20px">${typeIcon(r.file_type||'')}</span>
1883
- <div style="flex:1;min-width:0">
1884
- <div style="font-size:13px;font-weight:700;color:var(--text-1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${r.file_name||'Unknown'}</div>
1885
- <div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-4)">${r.case_id} Β· ${formatSize(r.file_size_bytes||0)}</div>
1886
- </div>
1887
- <div class="batch-verdict-badge" style="background:${bg};color:${col};border:1px solid ${col}33">${v}</div>
1888
- <div style="font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;color:${col}">${Math.round((r.confidence||0.5)*100)}%</div>
1889
- </div>`;
1890
- }).join('');
1891
- }
1892
- }
1893
-
1894
- function openBatchDetail(caseId){
1895
- if(!caseId) return;
1896
- // Download report if available
1897
- toast('⬇️ Downloading report for '+caseId,2000);
1898
- const a=document.createElement('a');
1899
- a.href=`${API_BASE}/report/${caseId}`;
1900
- a.target='_blank';
1901
- a.click();
1902
- }
1903
-
1904
- // ═══════════════════════════════════════════════════
1905
- // HISTORY β€” NEW
1906
- // ═══════════════════════════════════════════════════
1907
- let _historyData=[];
1908
- let _histFilter='all';
1909
-
1910
- async function loadHistory(){
1911
- try{
1912
- const resp=await fetch(`${API_BASE}/history?limit=100`);
1913
- if(!resp.ok) throw new Error('No history endpoint');
1914
- const data=await resp.json();
1915
- _historyData=data.cases||[];
1916
- updateHistoryStats(data);
1917
- renderHistoryGrid(_historyData);
1918
- }catch(e){
1919
- // Fallback: show cases from current session state
1920
- renderHistoryGrid([]);
1921
- }
1922
- }
1923
-
1924
- function updateHistoryStats(data){
1925
- const cases=data.cases||[];
1926
- document.getElementById('histTotal').textContent=data.total||cases.length||0;
1927
- document.getElementById('histFakes').textContent=cases.filter(c=>c.verdict==='FAKE'||c.verdict==='DEEPFAKE').length;
1928
- document.getElementById('histAuthentic').textContent=cases.filter(c=>c.verdict==='AUTHENTIC').length;
1929
- document.getElementById('histSynth').textContent=cases.filter(c=>c.verdict==='SYNTHETIC').length;
1930
- }
1931
-
1932
- function filterHistory(filter,btn){
1933
- _histFilter=filter;
1934
- document.querySelectorAll('.hist-filter-btn').forEach(b=>b.classList.remove('active'));
1935
- if(btn) btn.classList.add('active');
1936
- let filtered=_historyData;
1937
- if(filter!=='all'){
1938
- const f=filter.toLowerCase();
1939
- filtered=_historyData.filter(c=>
1940
- c.verdict.toLowerCase()===f ||
1941
- (c.file_type||'').toLowerCase().includes(f)
1942
- );
1943
- }
1944
- renderHistoryGrid(filtered);
1945
- }
1946
-
1947
- function renderHistoryGrid(cases){
1948
- const grid=document.getElementById('historyGrid');
1949
- if(!grid) return;
1950
- if(!cases||cases.length===0){
1951
- grid.innerHTML='<div class="history-empty">πŸ“­ No cases match this filter</div>';
1952
- return;
1953
- }
1954
- const typeIcon=t=>(t||'').startsWith('image')?'πŸ–ΌοΈ':(t||'').startsWith('video')?'πŸŽ₯':(t||'').startsWith('audio')?'🎡':'πŸ“„';
1955
- grid.innerHTML=cases.map(c=>{
1956
- const v=c.verdict||'UNKNOWN';
1957
- const isFake=v==='FAKE'||v==='DEEPFAKE';
1958
- const isSynth=v==='SYNTHETIC';
1959
- const col=isFake?'var(--red)':isSynth?'var(--synth)':v==='AUTHENTIC'?'var(--green)':'var(--text-4)';
1960
- const bg=isFake?'var(--red-pale)':isSynth?'var(--synth-pale)':v==='AUTHENTIC'?'var(--green-pale)':'rgba(0,0,0,0.06)';
1961
- const ts=c.timestamp?new Date(c.timestamp).toLocaleString():'Unknown time';
1962
- return `<div class="history-card" onclick="openHistoryCase('${c.case_id}')">
1963
- <span class="history-icon">${typeIcon(c.file_type)}</span>
1964
- <div class="history-info">
1965
- <div class="history-name">${c.file_name||'Unknown file'}</div>
1966
- <div class="history-meta">${c.case_id} Β· ${ts} Β· ${c.enabled_modules||46} modules</div>
1967
- ${c.ai_summary?`<div style="font-size:11px;color:var(--text-3);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:400px">${c.ai_summary}</div>`:''}
1968
- </div>
1969
- <div class="history-verdict" style="background:${bg};color:${col};border:1px solid ${col}33">${v}</div>
1970
- <div class="history-risk" style="color:${col}">${c.risk_score||0}%</div>
1971
- <button onclick="event.stopPropagation();deleteHistoryCase('${c.case_id}')" style="padding:4px 8px;border-radius:6px;background:rgba(220,38,38,0.06);border:1px solid rgba(220,38,38,0.15);color:var(--red);font-size:10px;cursor:none;transition:all 0.2s" title="Delete case">πŸ—‘οΈ</button>
1972
- </div>`;
1973
- }).join('');
1974
- }
1975
-
1976
- function openHistoryCase(caseId){
1977
- if(!caseId) return;
1978
- const a=document.createElement('a');
1979
- a.href=`${API_BASE}/report/${caseId}`;
1980
- a.target='_blank';
1981
- a.click();
1982
- toast('πŸ“„ Opening report for '+caseId,2000);
1983
- }
1984
-
1985
- async function deleteHistoryCase(caseId){
1986
- try{
1987
- const resp=await fetch(`${API_BASE}/history/${caseId}`,{method:'DELETE'});
1988
- if(resp.ok){
1989
- toast('πŸ—‘οΈ Case deleted',1500);
1990
- await loadHistory();
1991
- }
1992
- }catch(e){toast('❌ Could not delete case',2000);}
1993
- }
1994
-
1995
- async function clearHistory(){
1996
- if(!confirm('Clear all case history?')) return;
1997
- try{
1998
- await fetch(`${API_BASE}/history`,{method:'DELETE'});
1999
- _historyData=[];
2000
- renderHistoryGrid([]);
2001
- ['histTotal','histFakes','histAuthentic','histSynth'].forEach(id=>{
2002
- const el=document.getElementById(id);if(el)el.textContent='0';
2003
- });
2004
- toast('πŸ—‘οΈ History cleared',2000);
2005
- }catch(e){toast('❌ Could not clear history',2000);}
2006
- }
2007
-
2008
- // ═══════════════════════════════════════════════════
2009
- // HEALTH CHECK ON LOAD
2010
- // ═══════════════════════════════════════════════════
2011
- async function checkHealth(){
2012
- try{
2013
- const resp=await fetch(`${API_BASE}/health`);
2014
- if(!resp.ok) return;
2015
- const data=await resp.json();
2016
- const badge=document.getElementById('healthBadge');
2017
- if(badge){
2018
- badge.innerHTML=`<div class="dot-live"></div><span>${data.gpu?'GPU':'CPU'} Β· v${data.version}</span>`;
2019
- badge.title='VERIDEX backend online';
2020
- }
2021
- }catch(e){
2022
- const badge=document.getElementById('healthBadge');
2023
- if(badge){
2024
- badge.innerHTML='<div style="width:7px;height:7px;border-radius:50%;background:var(--red)"></div><span style="color:var(--red)">Offline</span>';
2025
- }
2026
- }
2027
- }
2028
-
2029
- setTimeout(checkHealth,800);
2030
  setTimeout(drawIdleCanvases,300);
2031
  setTimeout(drawIdleCanvases,800);
2032
  </script>
 
63
  /* BG */
64
  .bg-layer{position:fixed;inset:0;z-index:0;pointer-events:none;
65
  background:
66
+ radial-gradient(ellipse 70% 55% at 5% 0%,rgba(79,172,254,0.22) 0%,transparent 60%),
67
+ radial-gradient(ellipse 60% 50% at 95% 100%,rgba(0,242,254,0.18) 0%,transparent 55%),
68
+ radial-gradient(ellipse 50% 50% at 50% 50%,rgba(37,99,235,0.08) 0%,transparent 65%),
69
+ repeating-linear-gradient(0deg,rgba(79,172,254,0.07) 0px,rgba(79,172,254,0.07) 1px,transparent 1px,transparent 24px),
70
+ repeating-linear-gradient(90deg,rgba(79,172,254,0.07) 0px,rgba(79,172,254,0.07) 1px,transparent 1px,transparent 24px),
71
+ linear-gradient(135deg,rgba(224,240,255,0.93) 0%,rgba(224,254,255,0.90) 50%,rgba(230,242,255,0.92) 100%);}
72
  .grain{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:0.03;
73
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)'/%3E%3C/svg%3E");
74
  background-size:200px;}
75
  .blob{position:fixed;border-radius:50%;filter:blur(90px);pointer-events:none;z-index:0;animation:blobFloat 18s ease-in-out infinite;}
76
+ .b1{width:700px;height:700px;top:-200px;left:-200px;background:radial-gradient(circle,rgba(79,172,254,0.28),transparent 65%);}
77
+ .b2{width:500px;height:500px;bottom:-100px;right:-100px;background:radial-gradient(circle,rgba(0,242,254,0.24),transparent 65%);animation-delay:-9s;}
78
+ .b3{width:400px;height:400px;top:45%;left:40%;background:radial-gradient(circle,rgba(37,99,235,0.18),transparent 65%);animation-delay:-15s;}
79
  @keyframes blobFloat{0%,100%{transform:translate(0,0)scale(1)}33%{transform:translate(50px,-40px)scale(1.06)}66%{transform:translate(-30px,30px)scale(0.94)}}
80
 
81
  /* CURSOR */
82
  #cur{position:fixed;width:10px;height:10px;background:var(--blue);border-radius:50%;pointer-events:none;z-index:99999;transform:translate(-50%,-50%);transition:width .2s var(--ease),height .2s var(--ease),opacity .2s;mix-blend-mode:normal;}
83
+ #cur-ring{position:fixed;width:30px;height:30px;border:1.5px solid rgba(0,242,254,0.7);border-radius:50%;pointer-events:none;z-index:99998;transform:translate(-50%,-50%);transition:all .1s ease;}
84
  .h-active #cur{width:22px;height:22px;}
85
+ .c-active #cur{width:14px;height:14px;background:rgba(0,242,254,0.8);}
86
 
87
  /* LAYOUT */
88
  .app{position:relative;z-index:2;min-height:100vh;}
89
  .container{max-width:1280px;margin:0 auto;padding:0 40px;}
90
 
91
  /* HEADER */
92
+ header{position:sticky;top:0;z-index:100;backdrop-filter:blur(24px) saturate(180%);-webkit-backdrop-filter:blur(24px) saturate(180%);background:rgba(224,240,255,0.55);border-bottom:1px solid rgba(255,255,255,0.3);box-shadow:0 1px 24px rgba(79,172,254,0.2),0 1px 0 rgba(0,242,254,0.1);backdrop-filter:blur(24px) saturate(200%);-webkit-backdrop-filter:blur(24px) saturate(200%);}
93
  .header-inner{display:flex;align-items:center;justify-content:space-between;height:64px;}
94
  .logo{display:flex;align-items:center;gap:10px;font-family:'Playfair Display',serif;font-weight:700;font-size:20px;letter-spacing:0.05em;color:var(--text-1);}
95
+ .logo-icon{width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,#4FACFE,#00F2FE);display:flex;align-items:center;justify-content:center;font-size:16px;box-shadow:0 4px 14px rgba(79,172,254,0.5);}
96
  .logo-sub{font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:400;color:var(--text-4);letter-spacing:0.15em;display:block;margin-top:1px;}
97
  .header-center{display:flex;gap:4px;}
98
  .nav-btn{padding:7px 16px;border-radius:100px;font-size:13px;font-weight:500;color:var(--text-3);letter-spacing:0.01em;transition:all 0.2s;cursor:none;border:1px solid transparent;}
99
  .nav-btn:hover,.nav-btn.active{background:rgba(255,255,255,0.2);border-color:rgba(255,255,255,0.35);color:var(--text-1);box-shadow:var(--shadow-sm);}
100
  .nav-btn.active{color:var(--blue);}
101
  .header-right{display:flex;align-items:center;gap:10px;}
102
+ .status-badge{display:flex;align-items:center;gap:6px;padding:6px 12px;border-radius:100px;background:rgba(255,255,255,0.30);border:1px solid rgba(79,172,254,0.3);box-shadow:var(--shadow-sm);font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-3);}
103
  .dot-live{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:liveGlow 2s ease-in-out infinite;}
104
  @keyframes liveGlow{0%,100%{opacity:1}50%{opacity:0.5}}
105
 
106
  /* HERO */
107
  .hero{padding:40px 0 20px;}
108
+ .hero-title{font-family:'Playfair Display',serif;font-size:42px;font-weight:900;letter-spacing:-0.02em;line-height:1.1;background:linear-gradient(135deg,#2563EB 0%,#4FACFE 50%,#00F2FE 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:10px;}
109
  .hero-sub{font-size:15px;color:var(--text-3);font-weight:400;line-height:1.5;}
110
  .hero-badges{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px;}
111
+ .hero-badge{display:flex;align-items:center;gap:5px;padding:5px 12px;border-radius:100px;font-size:11px;font-weight:600;letter-spacing:0.04em;background:rgba(255,255,255,0.28);border:1px solid rgba(79,172,254,0.25);box-shadow:var(--shadow-sm);color:var(--text-1);animation:badgeFade 0.5s var(--ease) backwards;}
112
  .hero-badge:nth-child(1){animation-delay:.05s}
113
  .hero-badge:nth-child(2){animation-delay:.1s}
114
  .hero-badge:nth-child(3){animation-delay:.15s}
 
261
  .mod-card.running .mod-status{background:var(--amber);box-shadow:0 0 8px var(--amber);animation:liveGlow 0.8s ease-in-out infinite;}
262
  .mod-card.disabled-mod{opacity:0.4;filter:grayscale(0.6);}
263
  .mod-card.disabled-mod .mod-status{background:rgba(0,0,0,0.1);}
264
+ .mod-card.mod-flagged{border-color:rgba(220,38,38,0.35)!important;background:rgba(220,38,38,0.06)!important;}
265
+ .mod-card.mod-flagged .mod-status{background:var(--red)!important;box-shadow:0 0 8px var(--red)!important;}
266
  .mod-icon{font-size:18px;margin-bottom:6px;display:block;}
267
  .mod-name{font-size:10px;font-weight:700;color:var(--text-2);letter-spacing:0.04em;margin-bottom:2px;}
268
  .mod-desc{font-size:9px;color:var(--text-4);line-height:1.4;}
 
428
  .fade-in:nth-child(1){animation-delay:.05s}
429
  .fade-in:nth-child(2){animation-delay:.1s}
430
  @keyframes fadeIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  </style>
432
  </head>
433
  <body>
 
455
  <div class="header-inner">
456
  <div class="logo">
457
  <div class="logo-icon">πŸ”¬</div>
458
+ <div>VERIDEX <span class="logo-sub">FORENSIC AI PLATFORM v2</span></div>
459
  </div>
460
  <nav class="header-center">
461
  <button class="nav-btn active" onclick="showSection('analyze')">Analyze</button>
 
462
  <button class="nav-btn" onclick="showSection('modules')">46 Modules</button>
 
463
  <button class="nav-btn" onclick="showSection('custody')">Chain of Custody</button>
464
  </nav>
465
  <div class="header-right">
466
+ <div class="status-badge"><div class="dot-live"></div><span id="activeModCount">46</span>/46 Active</div>
467
  </div>
468
  </div>
469
  </div>
 
604
  <button class="btn-report" id="btnReport" disabled onclick="openReport()">πŸ“„ GENERATE COURT REPORT</button>
605
  </div>
606
 
 
 
 
 
 
 
 
 
 
 
607
  <!-- METADATA -->
608
  <div class="card fade-in">
609
  <div class="card-header"><div class="card-title"><div class="card-title-icon" style="background:rgba(13,148,136,0.08)">πŸ—‚οΈ</div>File Metadata</div></div>
 
622
  </div>
623
  </div>
624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  <!-- MODULES SECTION -->
626
  <div id="sec-modules" style="display:none;padding-bottom:40px">
627
  <div class="sec-lbl">All 46 Detection Modules</div>
 
1153
  function resetFrameBars(){Array.from({length:80},(_,i)=>{const fb=document.getElementById('fb'+i);if(fb){fb.className='fb idle';fb.style.height=(20+Math.random()*15)+'px';}});}
1154
 
1155
  // ═══════════════════════════════════════════════════
1156
+ // ANALYSIS
 
 
1157
  // ═══════════════════════════════════════════════════
1158
+ // ═══════════════════════════════════════════════════
1159
+ // BACKEND API CONFIG β€” smart detection
1160
+ // ═══════════════════════════════════════════════════
1161
+ // If served from the FastAPI backend (port 8000), same-origin works.
1162
+ // If opened from file:// or python -m http.server, try localhost:8000.
1163
+ const API_BASE = (()=>{
1164
+ const loc = window.location;
1165
+ // If we're already on port 8000 or 8001 (uvicorn), use same origin
1166
+ if(loc.port==='8000'||loc.port==='8001'||loc.hostname==='veridex.up.railway.app') return '';
1167
+ // Otherwise target the FastAPI backend on 8000
1168
+ return 'http://localhost:8000';
1169
+ })();
1170
 
1171
  // ═══════════════════════════════════════════════════
1172
+ // SEEDED SIMULATION β€” used when backend is offline
1173
+ // Deterministic per filename so same file = same result
1174
+ // ═══════════════════════════════════════════════════
1175
+ function seededRandom(seed){
1176
+ let s=seed;
1177
+ return function(){s=Math.sin(s)*10000;return s-Math.floor(s);};
1178
+ }
1179
+ function simulateAllModules(fileName,fileSize,fileType){
1180
+ // Generate a consistent seed from file properties
1181
+ let seed=0;for(let i=0;i<fileName.length;i++)seed+=fileName.charCodeAt(i);seed+=fileSize%997;
1182
+ const rng=seededRandom(seed);
1183
+
1184
+ // Generate per-module scores (0.0–1.0) seeded
1185
+ const moduleScores={};
1186
+ const base=rng(); // 0-1, determines authentic vs fake tendency
1187
+ const isFake=base<0.45;
1188
+ const isSynth=!isFake&&base<0.65;
1189
+
1190
+ for(let m=1;m<=46;m++){
1191
+ const r=rng();
1192
+ if(m===1||m===2) moduleScores[m]=1.0; // Legal/hash = always valid
1193
+ else if(isFake) moduleScores[m]=Math.max(0.05,Math.min(0.95,0.35+r*0.4-0.2));
1194
+ else if(isSynth&&m===46) moduleScores[m]=Math.max(0.05,Math.min(0.95,0.25+r*0.3));
1195
+ else moduleScores[m]=Math.max(0.05,Math.min(0.95,0.65+r*0.35-0.1));
1196
+ }
1197
+
1198
+ // Weighted confidence
1199
+ const weights={6:0.12,7:0.10,8:0.09,15:0.11,46:0.14,9:0.07};
1200
+ const defaultW=0.02;
1201
+ let tw=0,ws=0;
1202
+ for(let m=1;m<=46;m++){const w=weights[m]||defaultW;tw+=w;ws+=w*moduleScores[m];}
1203
+ const avg=ws/tw;
1204
+ const confidence=Math.round(avg*100)/100;
1205
+ const verdict=avg<0.40?'FAKE':isSynth?'SYNTHETIC':'AUTHENTIC';
1206
+
1207
+ const moduleNames={6:'RGB Forensic',7:'DCT/FFT Analysis',8:'Noise Residual',9:'rPPG Signal',15:'GAN Artifacts',11:'Lip Sync',16:'Metadata',46:'Synth ID'};
1208
+ const anomalies=Object.entries(moduleScores).filter(([,v])=>v<0.4).sort(([,a],[,b])=>a-b).slice(0,4);
1209
+ const findings=anomalies.length?anomalies.map(([m])=>`Module ${m} (${moduleNames[m]||'Forensic'}): anomaly flagged β€” ${Math.round((1-moduleScores[m])*100)}% confidence`):['All modules within authentic parameters'];
1210
+
1211
+ const scoresStr={};
1212
+ Object.entries(moduleScores).forEach(([k,v])=>scoresStr[k]=v.toFixed(4));
1213
+
1214
+ return {
1215
+ case_id:'VRX-SIM-'+Date.now(),
1216
+ sha256:null,
1217
+ timestamp:new Date().toLocaleString(),
1218
+ file_name:fileName,file_type:fileType,
1219
+ verdict,
1220
+ confidence,
1221
+ is_synth:isSynth,
1222
+ synth_data:isSynth?{
1223
+ generator:['Stable Diffusion XL','DALL-E 3','Midjourney v6','Adobe Firefly'][Math.floor(rng()*4)],
1224
+ provider:['Stability AI','OpenAI','Midjourney Inc','Adobe'][Math.floor(rng()*4)],
1225
+ confidence:0.7+rng()*0.25,
1226
+ watermark_detected:rng()>0.5,
1227
+ signal_type:['GAN fingerprint','Diffusion trace','VAE artifact','CLIP embedding'][Math.floor(rng()*4)]
1228
+ }:{},
1229
+ scores:scoresStr,
1230
+ key_findings:findings,
1231
+ ai_summary:`[OFFLINE MODE] Seeded forensic simulation β€” connect backend for real analysis`,
1232
+ _simulated:true
1233
+ };
1234
+ }
1235
+
1236
+ // ═══════════════════════════════════════════════════
1237
+ // REAL BACKEND ANALYSIS with auto-fallback
1238
  // ═══════════════════════════════════════════════════
1239
  async function startAnalysis(){
1240
  if(!state.file){toast('⚠️ Please upload a file first',2500);return;}
 
1264
  if(pct>15 && msgIdx<msgs.length){toast(msgs[msgIdx++],1200);}
1265
  },400);
1266
 
1267
+ let data=null;
1268
+ let usedFallback=false;
1269
+
1270
  try {
1271
  const formData=new FormData();
1272
  formData.append('file', state.file);
1273
  const enabledIds=active.map(m=>m.id).join(',');
1274
  formData.append('modules', enabledIds);
1275
 
1276
+ const response = await fetch(`${API_BASE}/analyze`, {method:'POST',body:formData});
 
1277
 
1278
+ clearInterval(interval);
1279
  if(!response.ok){
1280
  let errMsg='Analysis failed';
1281
  try{const err=await response.json();errMsg=err.detail||errMsg;}catch(e){}
1282
  throw new Error(`Server ${response.status}: ${errMsg}`);
1283
  }
1284
+ data = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1285
 
1286
  } catch(err) {
1287
  clearInterval(interval);
1288
+ const isNetwork=err.message.includes('Failed to fetch')||err.message.includes('NetworkError')||err.message.includes('fetch')||err.message.includes('Load failed');
 
 
 
 
 
1289
  if(isNetwork){
1290
+ // AUTO FALLBACK: seeded simulation β€” no error shown
1291
+ toast('πŸ“‘ Backend offline β€” using seeded forensic simulation',3000);
1292
+ data = simulateAllModules(state.file.name, state.file.size, state.file.type||'unknown');
1293
+ usedFallback=true;
1294
  } else {
1295
+ btn.classList.remove('loading');
1296
+ document.getElementById('scanBeam').classList.remove('active');
1297
+ progBar.style.width='0%'; progWrap.classList.remove('active');
1298
+ active.forEach(m=>{const el=document.getElementById('mod'+m.id);if(el)el.className='mod-card';});
1299
+ toast('❌ '+err.message,5000);
1300
+ console.error('[VERIDEX] Analysis error:',err);
1301
+ return;
1302
  }
 
1303
  }
1304
+
1305
+ progBar.style.width='100%';
1306
+
1307
+ state.analysisId = data.case_id;
1308
+ state.timestamp = data.timestamp;
1309
+ state.apiData = data;
1310
+
1311
+ if(data.sha256){
1312
+ state.hash = data.sha256;
1313
+ document.getElementById('hashDisplay').innerHTML=`<strong>SHA-256:</strong><br>${data.sha256}`;
1314
+ document.getElementById('hashStatus').textContent='βœ…';
1315
+ document.getElementById('hashStatusTxt').textContent=usedFallback?'Client-computed hash':'Server-verified integrity';
1316
+ document.getElementById('chash1').textContent=data.sha256.slice(0,16)+'...';
1317
+ markCustodyStep(1,data.sha256.slice(0,16)+'...');
1318
+ }
1319
+
1320
+ // Map ALL 46 module scores to UI
1321
+ const scores=data.scores||{};
1322
+ Object.entries(scores).forEach(([k,v])=>{
1323
+ const mid=parseInt(k);
1324
+ if(!isNaN(mid)) state.moduleResults[mid]=Math.round(parseFloat(v)*100);
1325
+ });
1326
+
1327
+ // Animate ALL active module cards with their real/seeded scores
1328
+ active.forEach((m,i)=>{
1329
+ setTimeout(()=>{
1330
+ const el=document.getElementById('mod'+m.id);
1331
+ const fp=document.getElementById('fp'+m.id);
1332
+ const score=state.moduleResults[m.id]||70;
1333
+ if(el) el.className='mod-card on'+(score<40?' mod-flagged':'');
1334
+ if(fp) fp.className='feat-pip on';
1335
+ },i*40);
1336
+ });
1337
+
1338
+ document.getElementById('scanBeam').classList.remove('active');
1339
+ btn.classList.remove('loading');
1340
+
1341
+ const verdict = data.verdict||'UNKNOWN';
1342
+ const isFake = verdict==='FAKE'||verdict==='DEEPFAKE';
1343
+ const isSynth = data.is_synth||verdict==='SYNTHETIC';
1344
+ const score = Math.round(data.confidence*100);
1345
+
1346
+ finishAnalysisFromAPI(data, score, isFake, isSynth);
1347
  }
1348
 
1349
+ // Uses REAL or SIMULATED data β€” both use the same response shape
 
 
1350
  function finishAnalysisFromAPI(data, score, isFake, isSynth){
1351
  state.finalScore=score; state.isFake=isFake; state.isSynth=isSynth; state.analyzed=true;
1352
 
 
1363
  else if(isSynth){chip.className='verdict-chip synth';chip.textContent='🧬 SYNTHETIC DETECTED';}
1364
  else{chip.className='verdict-chip authentic';chip.textContent='βœ… AUTHENTIC MEDIA';}
1365
 
1366
+ // ── 8 sub-score bars ──
1367
  const scores=data.scores||{};
1368
  const subMap=[
1369
  ['6','RGB Forensic'],['7','Frequency'],['8','Noise Residual'],['9','rPPG Signal'],
 
1383
  },i*150);
1384
  });
1385
 
1386
+ // ── All 46 module cards: update color based on score ──
1387
+ Object.entries(scores).forEach(([k,v])=>{
1388
+ const mid=parseInt(k); if(isNaN(mid))return;
1389
+ const val=Math.round(parseFloat(v)*100);
1390
+ state.moduleResults[mid]=val;
1391
+ // Already animated in startAnalysis; update flagged class here
1392
+ const el=document.getElementById('mod'+mid);
1393
+ if(el && el.classList.contains('on')){
1394
+ if(val<40) el.classList.add('mod-flagged');
1395
+ }
1396
+ });
1397
+
1398
+ // ── Synth ID card ──
1399
  if(isSynth && data.synth_data && Object.keys(data.synth_data).length>0){
1400
  const sd=data.synth_data;
1401
  state.synthData={
1402
  model:sd.generator||'Unknown',generator:sd.provider||'Unknown',
1403
+ confidence:Math.round((sd.confidence||0)*(sd.confidence>1?1:100)),
1404
  faceMatch:'GENERATED',watermark:sd.watermark_detected?'DETECTED':'NOT FOUND',
1405
  signalType:sd.signal_type||'Unknown',
1406
  };
 
1413
  setTimeout(()=>{document.getElementById('synthConfFill').style.width=state.synthData.confidence+'%';},300);
1414
  }
1415
 
1416
+ // ── Key findings in metadata panel ──
1417
  if(data.key_findings && data.key_findings.length>0){
1418
  const meta=document.getElementById('metaPanel');
1419
+ const findingsHtml=data.key_findings.map(f=>`<div class="rep-field" style="grid-column:1/-1"><div class="rep-f-lbl">πŸ” FINDING</div><div class="rep-f-val" style="font-size:11px;color:var(--text-2)">${f}</div></div>`).join('');
1420
+ meta.innerHTML=(meta.innerHTML||'')+findingsHtml;
1421
  }
1422
 
1423
+ // ── Simulation badge ──
1424
+ if(data._simulated){
1425
+ const meta=document.getElementById('metaPanel');
1426
+ if(meta) meta.innerHTML=`<div class="rep-field" style="grid-column:1/-1;background:var(--amber-pale);border-color:rgba(217,119,6,0.25)"><div class="rep-f-lbl">⚠️ OFFLINE MODE</div><div class="rep-f-val" style="font-size:11px;color:var(--amber)">Backend offline β€” seeded forensic simulation. Start uvicorn for real AI analysis.</div></div>`+(meta.innerHTML||'');
 
 
1427
  }
1428
 
1429
  animateFrameBars(isFake);
 
1433
  document.getElementById('btnReport').disabled=false;
1434
  document.getElementById('progWrap').classList.remove('active');
1435
 
1436
+ const modeTag=data._simulated?' [SIMULATED]':'';
1437
+ toast(isFake?'⚠️ DEEPFAKE DETECTED'+modeTag:isSynth?'🧬 Synthetic media detected'+modeTag:'βœ… Authentic media confirmed'+modeTag,4000);
1438
+ }
1439
+
1440
+ function finishAnalysis(score,isFake,isSynth){
1441
+ state.finalScore=score;state.isFake=isFake;state.isSynth=isSynth;state.analyzed=true;
1442
+ state.analysisId='VRX-'+Date.now()+'-'+Math.random().toString(36).slice(2,8).toUpperCase();
1443
+ state.timestamp=new Date().toISOString();
1444
+
1445
+ const offset=326-(326*score/100);
1446
+ const fill=document.getElementById('ringFill');
1447
+ fill.style.strokeDashoffset=offset;
1448
+ fill.style.stroke=isFake?'var(--red)':isSynth?'var(--synth)':'var(--green)';
1449
+
1450
+ document.getElementById('scoreNum').textContent=score+'%';
1451
+ document.getElementById('scoreNum').style.color=isFake?'var(--red)':isSynth?'var(--synth)':'var(--green)';
1452
+
1453
+ const chip=document.getElementById('verdictChip');
1454
+ if(isFake){chip.className='verdict-chip fake';chip.textContent='⚠️ DEEPFAKE DETECTED';}
1455
+ else if(isSynth){chip.className='verdict-chip synth';chip.textContent='🧬 SYNTHETIC ID DETECTED';}
1456
+ else{chip.className='verdict-chip authentic';chip.textContent='βœ… AUTHENTIC MEDIA';}
1457
+
1458
+ const sData=generateSubScores(isFake,isSynth);
1459
+ state.scores=sData;
1460
+ Object.entries(sData).forEach(([key,val],i)=>{
1461
+ const idx=i+1;const el=document.getElementById('ss'+idx);const valEl=document.getElementById('sv'+idx);if(!el||!valEl)return;
1462
+ setTimeout(()=>{
1463
+ el.style.width=val+'%';valEl.textContent=val+'%';
1464
+ const cls=idx===8?'synth-fill':val<40?'danger':val<65?'warn':'safe';
1465
+ el.className='ss-fill '+cls;
1466
+ },i*150);
1467
+ });
1468
+
1469
+ // SYNTH ID Card
1470
+ if(isSynth){
1471
+ const synthScore=Math.floor(65+Math.random()*30);
1472
+ state.synthData={
1473
+ model:['DALL-E 3','Midjourney v6','Stable Diffusion XL','Adobe Firefly'][Math.floor(Math.random()*4)],
1474
+ confidence:synthScore,
1475
+ faceMatch:'GENERATED',
1476
+ watermark:Math.random()>0.5?'DETECTED':'NOT FOUND',
1477
+ signalType:['GAN fingerprint','Diffusion trace','VAE artifact','CLIP embedding'][Math.floor(Math.random()*4)],
1478
+ generator:['OpenAI','Stability AI','Adobe','Midjourney'][Math.floor(Math.random()*4)],
1479
+ };
1480
+ const sc=document.getElementById('synthCard');sc.classList.add('visible');
1481
+ document.getElementById('synthGrid').innerHTML=Object.entries({
1482
+ 'Generator Model':state.synthData.model,'Confidence':state.synthData.confidence+'%',
1483
+ 'Face Status':state.synthData.faceMatch,'Watermark':state.synthData.watermark,
1484
+ 'Signal Type':state.synthData.signalType,'Provider':state.synthData.generator,
1485
+ }).map(([k,v])=>`<div class="synth-item"><div class="synth-lbl">${k}</div><div class="synth-val">${v}</div></div>`).join('');
1486
+ setTimeout(()=>{document.getElementById('synthConfFill').style.width=state.synthData.confidence+'%';},300);
1487
  }
1488
 
1489
+ animateFrameBars(isFake);
1490
+ if(state.fileType.startsWith('image')) setTimeout(()=>addDetectionBoxes(isFake),500);
1491
+ setTimeout(()=>drawAnalysisCanvases(isFake,isSynth),300);
1492
+ markCustodyStep(2,'Completed');markCustodyStep(3,score+'%');
1493
+ document.getElementById('btnReport').disabled=false;
1494
+
1495
+ // Update video forensic panel if visible
1496
+ const vfg=document.getElementById('vForensicGrid');
1497
+ if(vfg){
1498
+ const vstf=vfg.querySelectorAll('.vstf');
1499
+ const results=isFake?['⚠️ ANOMALY','⚠️ MISMATCH','⚠️ BROKEN','⚠️ WARPED','⚠️ ABSENT','⚠️ LOW']:['βœ… STABLE','βœ… SYNCED','βœ… COHERENT','βœ… NATURAL','βœ… DETECTED','βœ… NORMAL'];
1500
+ vstf.forEach((c,i)=>{c.textContent=results[i];c.style.color=isFake?'var(--red)':'var(--green)';});
1501
+ }
1502
 
1503
+ toast(isFake?'⚠️ Deepfake indicators found!':isSynth?'🧬 Synthetic ID detected!':'βœ… Media appears authentic',3500);
1504
  }
1505
 
1506
+ function generateSubScores(isFake,isSynth){
1507
+ const base=isFake?25:75;
1508
+ const keys=['RGB Forensic','Frequency','Noise Residual','rPPG Signal','GAN Artifacts','Lip Sync','Metadata','Synth ID'];
1509
+ const out={};
1510
+ keys.forEach((k,i)=>{
1511
+ if(k==='Synth ID') out[k]=isSynth?Math.max(60,Math.min(98,75+Math.floor(Math.random()*20))):Math.max(5,Math.min(35,15+Math.floor(Math.random()*20)));
1512
+ else out[k]=Math.max(5,Math.min(98,base+Math.floor((Math.random()-0.5)*40)));
1513
+ });
1514
+ return out;
1515
+ }
1516
 
1517
  function animateFrameBars(isFake){
1518
  Array.from({length:80},(_,i)=>setTimeout(()=>{
 
1605
  // NAVIGATION
1606
  // ═══════════════════════════════════════════════════
1607
  function showSection(name){
1608
+ ['analyze','modules','custody'].forEach(s=>{document.getElementById('sec-'+s).style.display=s===name?'block':'none';});
1609
+ document.querySelectorAll('.nav-btn').forEach((b,i)=>{b.classList.toggle('active',b.textContent.toLowerCase().includes(name)||(i===0&&name==='analyze'));});
1610
+ if(name==='custody')updateCustodyLog();
 
 
 
 
 
 
 
 
 
1611
  window.scrollTo({top:0,behavior:'smooth'});
1612
  }
1613
 
 
1637
  <div class="rep-field"><div class="rep-f-lbl">Active Modules</div><div class="rep-f-val">${MODULES.length-state.disabledModules.size}/46</div></div>
1638
  </div></div>
1639
  <div class="report-section"><div class="rep-section-title">πŸ” Cryptographic Integrity</div>
1640
+ <div class="rep-hash"><strong>SHA-256:</strong><br>${state.hash||'N/A'}<br><br><strong>CHAIN OF CUSTODY:</strong> File β†’ Hash β†’ Analysis β†’ Verification β†’ Report<br><strong>TAMPER STATUS:</strong> βœ… UNMODIFIED</div>
1641
  </div>
1642
  <div class="report-section"><div class="rep-section-title">βš–οΈ Forensic Verdict</div>
1643
  <div style="display:flex;align-items:center;justify-content:space-between;padding:16px;border-radius:14px;background:${state.isFake?'var(--red-pale)':state.isSynth?'var(--synth-pale)':'var(--green-pale)'};border:1px solid ${state.isFake?'rgba(220,38,38,0.2)':state.isSynth?'rgba(139,92,246,0.2)':'rgba(22,163,74,0.2)'}">
 
1710
  function shortHash(){return Array.from({length:16},()=>'0123456789abcdef'[Math.floor(Math.random()*16)]).join('');}
1711
  function toast(msg,dur=2500){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),dur);}
1712
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1713
  setTimeout(drawIdleCanvases,300);
1714
  setTimeout(drawIdleCanvases,800);
1715
  </script>