ethix commited on
Commit
155fe98
·
1 Parent(s): 766d680

Implement REST API endpoints and JS event handlers for all XAI/Forensics features

Browse files
Files changed (2) hide show
  1. app.py +85 -66
  2. index.html +136 -13
app.py CHANGED
@@ -2,10 +2,10 @@ import os
2
  import base64
3
  from io import BytesIO
4
  from PIL import Image
 
 
5
  import gradio as gr
6
  from gradio import Server
7
- from gradio.data_classes import FileData
8
- from fastapi.responses import HTMLResponse
9
 
10
  from xai_engine.model import get_model
11
  from xai_engine.methods import run_all_methods, run_single_method, run_per_head_attention
@@ -18,7 +18,6 @@ app = Server(title="DeepfakeDetection-XAI")
18
  demo = app
19
 
20
 
21
-
22
  def b64_to_pil(b64_str: str) -> Image.Image:
23
  """Helper to convert base64 image data URL to PIL Image."""
24
  if not b64_str or not isinstance(b64_str, str):
@@ -31,20 +30,64 @@ def b64_to_pil(b64_str: str) -> Image.Image:
31
  return Image.open(BytesIO(image_data)).convert("RGB")
32
 
33
 
34
- @app.api(name="analyze")
35
- def analyze_api(image_b64: str, alpha: float = 0.6, cmap: str = "jet") -> dict:
36
- """
37
- Run complete ViT Deepfake analysis + 6 attribution methods.
38
- Returns prediction summary and list of attribution map overlays.
39
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  try:
41
- image = b64_to_pil(image_b64)
42
  except Exception as e:
43
  return {"status": "error", "message": f"Image decoding failed: {e}"}
44
 
45
  model = get_model()
46
  pred_data = model.predict(image)
47
- results = run_all_methods(model, image, alpha=alpha, cmap=cmap)
48
 
49
  res_dicts = [
50
  {
@@ -65,29 +108,19 @@ def analyze_api(image_b64: str, alpha: float = 0.6, cmap: str = "jet") -> dict:
65
  }
66
 
67
 
68
- @app.api(name="method_detail")
69
- def method_detail_api(
70
- image_b64: str,
71
- method_name: str,
72
- steps: int = 20,
73
- patch_size: int = 32,
74
- stride: int = 16,
75
- target_layer_idx: int = 11,
76
- alpha: float = 0.6,
77
- cmap: str = "jet",
78
- ) -> dict:
79
- """Deep-dive analysis for a single specified XAI method with custom parameters."""
80
  try:
81
- image = b64_to_pil(image_b64)
82
  except Exception as e:
83
  return {"status": "error", "message": f"Image decoding failed: {e}"}
84
 
85
  model = get_model()
86
  res = run_single_method(
87
- model, image, method_name,
88
- alpha=alpha, cmap=cmap, steps=steps,
89
- patch_size=patch_size, stride=stride,
90
- target_layer_idx=target_layer_idx,
91
  )
92
 
93
  return {
@@ -103,76 +136,59 @@ def method_detail_api(
103
  }
104
 
105
 
106
- @app.api(name="heads")
107
- def heads_api(image_b64: str, layer_idx: int = 11, cmap: str = "jet") -> dict:
108
- """Extract and visualize all 6 attention heads for a selected transformer layer."""
109
  try:
110
- image = b64_to_pil(image_b64)
111
  except Exception as e:
112
  return {"status": "error", "message": f"Image decoding failed: {e}"}
113
 
114
  model = get_model()
115
- head_data = run_per_head_attention(model, image, layer_idx=layer_idx, cmap=cmap)
116
  head_data["status"] = "success"
117
  return head_data
118
 
119
 
120
- @app.api(name="forensic")
121
- def forensic_api(
122
- image_b64: str,
123
- tool_name: str,
124
- quality: int = 75,
125
- scale: int = 50,
126
- contrast: int = 20,
127
- gradient_intensity: int = 90,
128
- blue_mode: str = "Abs",
129
- channel: str = "Luminance",
130
- bit: int = 0,
131
- minmax_radius: int = 2,
132
- ) -> dict:
133
- """Run classic digital forensic analysis (ELA, Gradient, BitPlane, MinMax, Wavelet)."""
134
  try:
135
- image = b64_to_pil(image_b64)
136
  except Exception as e:
137
  return {"status": "error", "message": f"Image decoding failed: {e}"}
138
 
139
  res = run_forensic_tool(
140
- image, tool_name,
141
- quality=quality, scale=scale, contrast=contrast,
142
- gradient_intensity=gradient_intensity, blue_mode=blue_mode,
143
- channel=channel, bit=bit, minmax_radius=minmax_radius,
144
  )
145
  res["status"] = "success"
146
  return res
147
 
148
 
149
- @app.api(name="report")
150
- def report_api(image_b64: str) -> FileData:
151
- """Generate and return downloadable multi-panel forensic report PNG."""
152
- image = b64_to_pil(image_b64)
153
  model = get_model()
154
  pred_data = model.predict(image)
155
  results = run_all_methods(model, image)
156
  report_path = generate_forensic_report(image, pred_data, results)
157
- return FileData(path=report_path)
 
 
158
 
159
 
160
- @app.api(name="submit_run")
161
- def submit_run_api(
162
- image_b64: str,
163
- ground_truth: str = "unknown",
164
- opt_in_image: bool = True,
165
- ) -> dict:
166
- """Log analysis run, prediction, ground truth feedback, and XAI composite heatmaps to dataset."""
167
  try:
168
- image = b64_to_pil(image_b64)
169
  except Exception as e:
170
  return {"status": "error", "message": f"Image decoding failed: {e}"}
171
 
172
  model = get_model()
173
  pred_data = model.predict(image)
174
  results = run_all_methods(model, image)
175
- run_id = log_run_dataset(image, pred_data, results, ground_truth=ground_truth, opt_in_image=opt_in_image)
176
 
177
  return {
178
  "status": "success",
@@ -212,5 +228,8 @@ async def homepage():
212
  return f.read()
213
 
214
 
 
 
 
215
  if __name__ == "__main__":
216
  app.launch(show_error=True)
 
2
  import base64
3
  from io import BytesIO
4
  from PIL import Image
5
+ from pydantic import BaseModel
6
+ from fastapi.responses import HTMLResponse
7
  import gradio as gr
8
  from gradio import Server
 
 
9
 
10
  from xai_engine.model import get_model
11
  from xai_engine.methods import run_all_methods, run_single_method, run_per_head_attention
 
18
  demo = app
19
 
20
 
 
21
  def b64_to_pil(b64_str: str) -> Image.Image:
22
  """Helper to convert base64 image data URL to PIL Image."""
23
  if not b64_str or not isinstance(b64_str, str):
 
30
  return Image.open(BytesIO(image_data)).convert("RGB")
31
 
32
 
33
+ # Pydantic Schemas for REST API Endpoints
34
+ class AnalyzeReq(BaseModel):
35
+ image_b64: str
36
+ alpha: float = 0.6
37
+ cmap: str = "jet"
38
+
39
+
40
+ class MethodDetailReq(BaseModel):
41
+ image_b64: str
42
+ method_name: str = "rollout"
43
+ steps: int = 20
44
+ patch_size: int = 32
45
+ stride: int = 16
46
+ target_layer_idx: int = 11
47
+ alpha: float = 0.6
48
+ cmap: str = "jet"
49
+
50
+
51
+ class HeadsReq(BaseModel):
52
+ image_b64: str
53
+ layer_idx: int = 11
54
+ cmap: str = "jet"
55
+
56
+
57
+ class ForensicReq(BaseModel):
58
+ image_b64: str
59
+ tool_name: str = "ela"
60
+ quality: int = 75
61
+ scale: int = 50
62
+ contrast: int = 20
63
+ gradient_intensity: int = 90
64
+ blue_mode: str = "Abs"
65
+ channel: str = "Luminance"
66
+ bit: int = 0
67
+ minmax_radius: int = 2
68
+
69
+
70
+ class ReportReq(BaseModel):
71
+ image_b64: str
72
+
73
+
74
+ class SubmitRunReq(BaseModel):
75
+ image_b64: str
76
+ ground_truth: str = "unknown"
77
+ opt_in_image: bool = True
78
+
79
+
80
+ # REST API Routes mounted on FastAPI app
81
+ @app.post("/api/analyze")
82
+ def analyze_post(req: AnalyzeReq) -> dict:
83
  try:
84
+ image = b64_to_pil(req.image_b64)
85
  except Exception as e:
86
  return {"status": "error", "message": f"Image decoding failed: {e}"}
87
 
88
  model = get_model()
89
  pred_data = model.predict(image)
90
+ results = run_all_methods(model, image, alpha=req.alpha, cmap=req.cmap)
91
 
92
  res_dicts = [
93
  {
 
108
  }
109
 
110
 
111
+ @app.post("/api/method_detail")
112
+ def method_detail_post(req: MethodDetailReq) -> dict:
 
 
 
 
 
 
 
 
 
 
113
  try:
114
+ image = b64_to_pil(req.image_b64)
115
  except Exception as e:
116
  return {"status": "error", "message": f"Image decoding failed: {e}"}
117
 
118
  model = get_model()
119
  res = run_single_method(
120
+ model, image, req.method_name,
121
+ alpha=req.alpha, cmap=req.cmap, steps=req.steps,
122
+ patch_size=req.patch_size, stride=req.stride,
123
+ target_layer_idx=req.target_layer_idx,
124
  )
125
 
126
  return {
 
136
  }
137
 
138
 
139
+ @app.post("/api/heads")
140
+ def heads_post(req: HeadsReq) -> dict:
 
141
  try:
142
+ image = b64_to_pil(req.image_b64)
143
  except Exception as e:
144
  return {"status": "error", "message": f"Image decoding failed: {e}"}
145
 
146
  model = get_model()
147
+ head_data = run_per_head_attention(model, image, layer_idx=req.layer_idx, cmap=req.cmap)
148
  head_data["status"] = "success"
149
  return head_data
150
 
151
 
152
+ @app.post("/api/forensic")
153
+ def forensic_post(req: ForensicReq) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
154
  try:
155
+ image = b64_to_pil(req.image_b64)
156
  except Exception as e:
157
  return {"status": "error", "message": f"Image decoding failed: {e}"}
158
 
159
  res = run_forensic_tool(
160
+ image, req.tool_name,
161
+ quality=req.quality, scale=req.scale, contrast=req.contrast,
162
+ gradient_intensity=req.gradient_intensity, blue_mode=req.blue_mode,
163
+ channel=req.channel, bit=req.bit, minmax_radius=req.minmax_radius,
164
  )
165
  res["status"] = "success"
166
  return res
167
 
168
 
169
+ @app.post("/api/report")
170
+ def report_post(req: ReportReq) -> dict:
171
+ image = b64_to_pil(req.image_b64)
 
172
  model = get_model()
173
  pred_data = model.predict(image)
174
  results = run_all_methods(model, image)
175
  report_path = generate_forensic_report(image, pred_data, results)
176
+ with open(report_path, "rb") as f:
177
+ rep_b64 = "data:image/png;base64," + base64.b64encode(f.read()).decode("utf-8")
178
+ return {"status": "success", "report_b64": rep_b64}
179
 
180
 
181
+ @app.post("/api/submit_run")
182
+ def submit_run_post(req: SubmitRunReq) -> dict:
 
 
 
 
 
183
  try:
184
+ image = b64_to_pil(req.image_b64)
185
  except Exception as e:
186
  return {"status": "error", "message": f"Image decoding failed: {e}"}
187
 
188
  model = get_model()
189
  pred_data = model.predict(image)
190
  results = run_all_methods(model, image)
191
+ run_id = log_run_dataset(image, pred_data, results, ground_truth=req.ground_truth, opt_in_image=req.opt_in_image)
192
 
193
  return {
194
  "status": "success",
 
228
  return f.read()
229
 
230
 
231
+ app.setup()
232
+
233
+
234
  if __name__ == "__main__":
235
  app.launch(show_error=True)
index.html CHANGED
@@ -422,6 +422,12 @@
422
  document.getElementById('tab-' + tabId).classList.add('border-teal-400', 'text-teal-400');
423
  document.getElementById('tab-' + tabId).classList.remove('border-transparent', 'text-slate-400');
424
  document.getElementById('section-' + tabId).classList.remove('hidden');
 
 
 
 
 
 
425
  }
426
 
427
  function handleFileSelect(evt) {
@@ -448,13 +454,12 @@
448
  }
449
 
450
  async function apiCall(endpoint, payload) {
451
- const resp = await fetch('/gradio_api/' + endpoint, {
452
  method: 'POST',
453
  headers: { 'Content-Type': 'application/json' },
454
- body: JSON.stringify({ data: [payload] })
455
  });
456
- const data = await resp.json();
457
- return data.data ? data.data[0] : data;
458
  }
459
 
460
  async function runAnalysis() {
@@ -464,11 +469,9 @@
464
  document.getElementById('btn-analyze').disabled = true;
465
 
466
  try {
467
- const resp = await fetch('/api/predict_all', { method: 'POST' }); // Fallback direct fetch
468
- const res = await apiCall('analyze', currentImageB64);
469
 
470
  if (res.status === 'success') {
471
- // Update Prediction Badge
472
  const pred = res.prediction;
473
  const isFake = pred.prediction === 'FAKE';
474
  const badge = document.getElementById('prediction-badge');
@@ -478,7 +481,6 @@
478
  document.getElementById('confidence-val').innerText = pred.confidence_pct.toFixed(2) + '%';
479
  document.getElementById('confidence-box').classList.remove('hidden');
480
 
481
- // Render Grid
482
  const grid = document.getElementById('results-grid');
483
  grid.innerHTML = '';
484
  res.results.forEach(r => {
@@ -498,24 +500,144 @@
498
 
499
  document.getElementById('submit-panel').classList.remove('hidden');
500
  document.getElementById('btn-report').disabled = false;
 
 
501
  }
502
  } catch (err) {
503
  console.error(err);
 
504
  } finally {
505
  document.getElementById('loading-spinner').classList.add('hidden');
506
  document.getElementById('btn-analyze').disabled = false;
507
  }
508
  }
509
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  async function submitDatasetRun() {
511
  if (!currentImageB64) return;
512
- const gt = document.querySelector('input[name="ground_truth"]:checked').value;
 
513
  const optIn = document.getElementById('opt-in-image').checked;
514
 
515
- const res = await apiCall('submit_run', currentImageB64);
516
- const statusP = document.getElementById('submit-status');
517
- statusP.innerText = `Run ${res.run_id} logged to dataset buildborderless/deepfake-explainability!`;
518
- statusP.classList.remove('hidden');
 
 
 
 
 
 
 
519
  }
520
 
521
  function openModal(title, b64) {
@@ -530,3 +652,4 @@
530
  </script>
531
  </body>
532
  </html>
 
 
422
  document.getElementById('tab-' + tabId).classList.add('border-teal-400', 'text-teal-400');
423
  document.getElementById('tab-' + tabId).classList.remove('border-transparent', 'text-slate-400');
424
  document.getElementById('section-' + tabId).classList.remove('hidden');
425
+
426
+ if (currentImageB64) {
427
+ if (tabId === 'explorer') runExplorerMethod();
428
+ if (tabId === 'heads') runPerHeadInspector();
429
+ if (tabId === 'forensics') runForensicAnalysis();
430
+ }
431
  }
432
 
433
  function handleFileSelect(evt) {
 
454
  }
455
 
456
  async function apiCall(endpoint, payload) {
457
+ const resp = await fetch('/api/' + endpoint, {
458
  method: 'POST',
459
  headers: { 'Content-Type': 'application/json' },
460
+ body: JSON.stringify(payload)
461
  });
462
+ return await resp.json();
 
463
  }
464
 
465
  async function runAnalysis() {
 
469
  document.getElementById('btn-analyze').disabled = true;
470
 
471
  try {
472
+ const res = await apiCall('analyze', { image_b64: currentImageB64, alpha: 0.6, cmap: 'jet' });
 
473
 
474
  if (res.status === 'success') {
 
475
  const pred = res.prediction;
476
  const isFake = pred.prediction === 'FAKE';
477
  const badge = document.getElementById('prediction-badge');
 
481
  document.getElementById('confidence-val').innerText = pred.confidence_pct.toFixed(2) + '%';
482
  document.getElementById('confidence-box').classList.remove('hidden');
483
 
 
484
  const grid = document.getElementById('results-grid');
485
  grid.innerHTML = '';
486
  res.results.forEach(r => {
 
500
 
501
  document.getElementById('submit-panel').classList.remove('hidden');
502
  document.getElementById('btn-report').disabled = false;
503
+ } else {
504
+ alert('Analysis error: ' + (res.message || 'Unknown error'));
505
  }
506
  } catch (err) {
507
  console.error(err);
508
+ alert('API Request Failed: ' + err);
509
  } finally {
510
  document.getElementById('loading-spinner').classList.add('hidden');
511
  document.getElementById('btn-analyze').disabled = false;
512
  }
513
  }
514
 
515
+ async function runExplorerMethod() {
516
+ if (!currentImageB64) return;
517
+ const method = document.getElementById('exp-method').value;
518
+ const cmap = document.getElementById('exp-cmap').value;
519
+ const alpha = parseFloat(document.getElementById('exp-alpha').value);
520
+ const steps = parseInt(document.getElementById('exp-steps').value);
521
+ const patch_size = parseInt(document.getElementById('exp-patch').value);
522
+ const target_layer_idx = parseInt(document.getElementById('exp-layer').value);
523
+
524
+ const ctrlIG = document.getElementById('ctrl-integrated');
525
+ if (ctrlIG) ctrlIG.classList.toggle('hidden', method !== 'integrated');
526
+ const ctrlOcc = document.getElementById('ctrl-occlusion');
527
+ if (ctrlOcc) ctrlOcc.classList.toggle('hidden', method !== 'occlusion');
528
+ const ctrlGC = document.getElementById('ctrl-gradcam');
529
+ if (ctrlGC) ctrlGC.classList.toggle('hidden', method !== 'gradcam');
530
+
531
+ const res = await apiCall('method_detail', {
532
+ image_b64: currentImageB64,
533
+ method_name: method,
534
+ steps: steps,
535
+ patch_size: patch_size,
536
+ target_layer_idx: target_layer_idx,
537
+ alpha: alpha,
538
+ cmap: cmap
539
+ });
540
+
541
+ if (res.status === 'success' && res.result) {
542
+ document.getElementById('exp-img-overlay').src = res.result.overlay_b64;
543
+ document.getElementById('exp-img-raw').src = res.result.raw_heatmap_b64;
544
+ }
545
+ }
546
+
547
+ async function runPerHeadInspector() {
548
+ if (!currentImageB64) return;
549
+ const layer_idx = parseInt(document.getElementById('head-layer-input').value);
550
+ document.getElementById('head-layer-val').innerText = layer_idx;
551
+
552
+ const res = await apiCall('heads', {
553
+ image_b64: currentImageB64,
554
+ layer_idx: layer_idx,
555
+ cmap: 'jet'
556
+ });
557
+
558
+ if (res.status === 'success' && res.head_overlays) {
559
+ const grid = document.getElementById('heads-grid');
560
+ grid.innerHTML = '';
561
+ res.head_overlays.forEach((b64, idx) => {
562
+ const card = document.createElement('div');
563
+ card.className = 'glass-panel rounded-xl p-2 text-center hover:border-teal-500/50 transition cursor-pointer';
564
+ card.onclick = () => openModal(`Layer ${layer_idx} — Head ${idx + 1}`, b64);
565
+ card.innerHTML = `
566
+ <img src="${b64}" class="w-full h-24 object-cover rounded-lg mb-1" />
567
+ <span class="text-[10px] font-mono text-slate-300">Head ${idx + 1}</span>
568
+ `;
569
+ grid.appendChild(card);
570
+ });
571
+ }
572
+ }
573
+
574
+ async function runForensicAnalysis() {
575
+ if (!currentImageB64) return;
576
+ const tool = document.getElementById('for-tool').value;
577
+ const quality = parseInt(document.getElementById('for-quality').value);
578
+ const bit = parseInt(document.getElementById('for-bit').value);
579
+
580
+ const ctrlELA = document.getElementById('ctrl-ela');
581
+ if (ctrlELA) ctrlELA.classList.toggle('hidden', tool !== 'ela');
582
+ const ctrlBit = document.getElementById('ctrl-bitplane');
583
+ if (ctrlBit) ctrlBit.classList.toggle('hidden', tool !== 'bitplane');
584
+
585
+ const res = await apiCall('forensic', {
586
+ image_b64: currentImageB64,
587
+ tool_name: tool,
588
+ quality: quality,
589
+ bit: bit
590
+ });
591
+
592
+ if (res.status === 'success' && res.result_b64) {
593
+ const img = document.getElementById('for-img-output');
594
+ img.src = res.result_b64;
595
+ img.classList.remove('hidden');
596
+ document.getElementById('for-placeholder').classList.add('hidden');
597
+ document.getElementById('for-info').innerText = `${res.tool_name} analysis complete (${res.compute_time_ms} ms).`;
598
+ }
599
+ }
600
+
601
+ async function generateReport() {
602
+ if (!currentImageB64) return;
603
+ const btn = document.getElementById('btn-report');
604
+ btn.disabled = true;
605
+ btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Generating...';
606
+
607
+ try {
608
+ const res = await apiCall('report', { image_b64: currentImageB64 });
609
+ if (res.status === 'success' && res.report_b64) {
610
+ openModal('Forensic Report', res.report_b64);
611
+ const a = document.createElement('a');
612
+ a.href = res.report_b64;
613
+ a.download = `forensic_report_${Date.now()}.png`;
614
+ a.click();
615
+ }
616
+ } catch (err) {
617
+ console.error(err);
618
+ } finally {
619
+ btn.disabled = false;
620
+ btn.innerHTML = '<i class="fa-solid fa-file-pdf"></i> Export Forensic Report';
621
+ }
622
+ }
623
+
624
  async function submitDatasetRun() {
625
  if (!currentImageB64) return;
626
+ const gtEl = document.querySelector('input[name="ground_truth"]:checked');
627
+ const gt = gtEl ? gtEl.value : 'unknown';
628
  const optIn = document.getElementById('opt-in-image').checked;
629
 
630
+ const res = await apiCall('submit_run', {
631
+ image_b64: currentImageB64,
632
+ ground_truth: gt,
633
+ opt_in_image: optIn
634
+ });
635
+
636
+ if (res.status === 'success') {
637
+ const statusP = document.getElementById('submit-status');
638
+ statusP.innerText = `Run ${res.run_id} logged to dataset buildborderless/deepfake-explainability!`;
639
+ statusP.classList.remove('hidden');
640
+ }
641
  }
642
 
643
  function openModal(title, b64) {
 
652
  </script>
653
  </body>
654
  </html>
655
+