anky2002 commited on
Commit
3601981
Β·
verified Β·
1 Parent(s): f71ced3

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +643 -0
app.py ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FORENSIQ β€” Main Gradio Application
3
+ Physics-Based, Multi-Agent Forensic Framework for Explainable Deepfake Detection
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import time
9
+ import numpy as np
10
+ import gradio as gr
11
+ import plotly.graph_objects as go
12
+ import plotly.express as px
13
+ from PIL import Image
14
+ from concurrent.futures import ThreadPoolExecutor, as_completed
15
+ from typing import List, Tuple, Any
16
+
17
+ # Import agents
18
+ from agents.optical_agent import run_optical_agent, AgentEvidence
19
+ from agents.sensor_agent import run_sensor_agent
20
+ from agents.model_agent import run_model_agent
21
+ from agents.statistical_agent import run_statistical_agent
22
+ from agents.semantic_agent import run_semantic_agent
23
+ from agents.metadata_agent import run_metadata_agent
24
+ from agents.text_agent import run_text_agent
25
+
26
+ # Import engine and explanation
27
+ from bayesian_engine import bayesian_synthesis, ForensicVerdict
28
+ from explanation import generate_forensic_report, generate_reasoning_tree, generate_court_brief
29
+
30
+
31
+ # ─── Agent Orchestrator ──────────────────────────────────────────────
32
+
33
+ def run_all_agents(img: Image.Image) -> Tuple[List[AgentEvidence], ForensicVerdict]:
34
+ """Run all 7 forensic agents in parallel and synthesize via Bayesian engine."""
35
+ if img is None:
36
+ raise ValueError("No image provided")
37
+
38
+ # Ensure RGB
39
+ if img.mode != "RGB":
40
+ img = img.convert("RGB")
41
+
42
+ # Resize if too large (for speed)
43
+ max_dim = 2048
44
+ w, h = img.size
45
+ if max(w, h) > max_dim:
46
+ ratio = max_dim / max(w, h)
47
+ img = img.resize((int(w * ratio), int(h * ratio)), Image.LANCZOS)
48
+
49
+ # Signal processing agents (fast, run in parallel)
50
+ signal_agents = [
51
+ ("optical", run_optical_agent),
52
+ ("sensor", run_sensor_agent),
53
+ ("model", run_model_agent),
54
+ ("statistical", run_statistical_agent),
55
+ ("metadata", run_metadata_agent),
56
+ ]
57
+
58
+ # VLM agents (slower, also parallel)
59
+ vlm_agents = [
60
+ ("semantic", run_semantic_agent),
61
+ ("text", run_text_agent),
62
+ ]
63
+
64
+ results = {}
65
+
66
+ with ThreadPoolExecutor(max_workers=7) as executor:
67
+ futures = {}
68
+ for name, fn in signal_agents + vlm_agents:
69
+ futures[executor.submit(fn, img)] = name
70
+
71
+ for future in as_completed(futures):
72
+ name = futures[future]
73
+ try:
74
+ results[name] = future.result()
75
+ except Exception as e:
76
+ # Create a failed agent evidence
77
+ results[name] = AgentEvidence(
78
+ agent_name=f"{name.title()} Agent (Error)",
79
+ violation_score=0.0,
80
+ confidence=0.0,
81
+ failure_prob=1.0,
82
+ rationale=f"Agent failed: {str(e)}",
83
+ )
84
+
85
+ # Order agents consistently
86
+ ordered = [
87
+ results.get("optical"),
88
+ results.get("sensor"),
89
+ results.get("model"),
90
+ results.get("statistical"),
91
+ results.get("semantic"),
92
+ results.get("metadata"),
93
+ results.get("text"),
94
+ ]
95
+ ordered = [r for r in ordered if r is not None]
96
+
97
+ # Bayesian synthesis
98
+ verdict = bayesian_synthesis(ordered)
99
+
100
+ # Generate explanations
101
+ verdict.forensic_report = generate_forensic_report(verdict)
102
+ reasoning = generate_reasoning_tree(verdict)
103
+ verdict.court_brief = generate_court_brief(verdict)
104
+
105
+ return ordered, verdict, reasoning
106
+
107
+
108
+ # ─── Visualization Functions ─────────────────────────────────────────
109
+
110
+ def create_gauge_chart(probability: float, verdict: str) -> go.Figure:
111
+ """Create a gauge chart for the overall probability."""
112
+ if probability > 0.65:
113
+ color = "red"
114
+ elif probability > 0.45:
115
+ color = "orange"
116
+ elif probability > 0.25:
117
+ color = "gold"
118
+ else:
119
+ color = "green"
120
+
121
+ fig = go.Figure(go.Indicator(
122
+ mode="gauge+number+delta",
123
+ value=probability * 100,
124
+ number={"suffix": "%", "font": {"size": 48}},
125
+ title={"text": f"Manipulation Probability<br><span style='font-size:0.7em;color:{color}'>{verdict}</span>",
126
+ "font": {"size": 18}},
127
+ gauge={
128
+ "axis": {"range": [0, 100], "tickwidth": 2},
129
+ "bar": {"color": color, "thickness": 0.3},
130
+ "bgcolor": "white",
131
+ "steps": [
132
+ {"range": [0, 25], "color": "rgba(0,180,0,0.15)"},
133
+ {"range": [25, 45], "color": "rgba(255,215,0,0.15)"},
134
+ {"range": [45, 65], "color": "rgba(255,165,0,0.15)"},
135
+ {"range": [65, 100], "color": "rgba(255,0,0,0.15)"},
136
+ ],
137
+ "threshold": {
138
+ "line": {"color": "black", "width": 3},
139
+ "thickness": 0.8,
140
+ "value": probability * 100,
141
+ },
142
+ },
143
+ ))
144
+ fig.update_layout(
145
+ height=280,
146
+ margin=dict(l=30, r=30, t=60, b=20),
147
+ paper_bgcolor="rgba(0,0,0,0)",
148
+ font={"family": "Inter, sans-serif"},
149
+ )
150
+ return fig
151
+
152
+
153
+ def create_radar_chart(agent_results: List[AgentEvidence]) -> go.Figure:
154
+ """Create radar chart showing all agent scores."""
155
+ names = []
156
+ scores = []
157
+ colors = []
158
+
159
+ for agent in agent_results:
160
+ short_name = agent.agent_name.replace(" Agent", "").replace(" Characteristics", "")
161
+ names.append(short_name)
162
+ # Map -1..+1 to 0..100 for display
163
+ display_score = (agent.violation_score + 1) * 50
164
+ scores.append(display_score)
165
+ if agent.violation_score > 0.2:
166
+ colors.append("red")
167
+ elif agent.violation_score < -0.1:
168
+ colors.append("green")
169
+ else:
170
+ colors.append("gold")
171
+
172
+ # Close the polygon
173
+ names_closed = names + [names[0]]
174
+ scores_closed = scores + [scores[0]]
175
+
176
+ fig = go.Figure()
177
+
178
+ fig.add_trace(go.Scatterpolar(
179
+ r=scores_closed,
180
+ theta=names_closed,
181
+ fill="toself",
182
+ fillcolor="rgba(255, 100, 100, 0.15)",
183
+ line=dict(color="rgba(255, 50, 50, 0.8)", width=2),
184
+ name="Violation Score",
185
+ ))
186
+
187
+ # Add neutral line at 50 (score = 0)
188
+ fig.add_trace(go.Scatterpolar(
189
+ r=[50] * (len(names) + 1),
190
+ theta=names_closed,
191
+ line=dict(color="gray", width=1, dash="dash"),
192
+ name="Neutral (score=0)",
193
+ showlegend=True,
194
+ ))
195
+
196
+ fig.update_layout(
197
+ polar=dict(
198
+ radialaxis=dict(
199
+ visible=True, range=[0, 100],
200
+ tickvals=[0, 25, 50, 75, 100],
201
+ ticktext=["Authentic", "", "Neutral", "", "Fake"],
202
+ ),
203
+ ),
204
+ height=400,
205
+ margin=dict(l=60, r=60, t=40, b=40),
206
+ paper_bgcolor="rgba(0,0,0,0)",
207
+ font={"family": "Inter, sans-serif", "size": 11},
208
+ showlegend=True,
209
+ legend=dict(x=0, y=-0.15, orientation="h"),
210
+ )
211
+ return fig
212
+
213
+
214
+ def create_agent_bar_chart(agent_results: List[AgentEvidence]) -> go.Figure:
215
+ """Create horizontal bar chart of agent scores."""
216
+ names = []
217
+ scores = []
218
+ colors = []
219
+ confidences = []
220
+
221
+ for agent in sorted(agent_results, key=lambda a: a.violation_score, reverse=True):
222
+ short = agent.agent_name.replace(" Agent", "")
223
+ names.append(short)
224
+ scores.append(agent.violation_score)
225
+ confidences.append(agent.confidence)
226
+ if agent.violation_score > 0.2:
227
+ colors.append("rgba(220, 53, 69, 0.8)")
228
+ elif agent.violation_score < -0.1:
229
+ colors.append("rgba(40, 167, 69, 0.8)")
230
+ else:
231
+ colors.append("rgba(255, 193, 7, 0.8)")
232
+
233
+ fig = go.Figure()
234
+ fig.add_trace(go.Bar(
235
+ y=names,
236
+ x=scores,
237
+ orientation="h",
238
+ marker_color=colors,
239
+ text=[f"{s:+.2f}" for s in scores],
240
+ textposition="outside",
241
+ ))
242
+
243
+ fig.add_vline(x=0, line_dash="dash", line_color="gray")
244
+ fig.update_layout(
245
+ xaxis=dict(title="Violation Score (-1=Authentic, +1=Fake)", range=[-1.1, 1.1]),
246
+ height=350,
247
+ margin=dict(l=150, r=50, t=20, b=40),
248
+ paper_bgcolor="rgba(0,0,0,0)",
249
+ font={"family": "Inter, sans-serif"},
250
+ )
251
+ return fig
252
+
253
+
254
+ def create_ela_display(agent_results: List[AgentEvidence]) -> Image.Image:
255
+ """Extract ELA image from metadata agent if available."""
256
+ for agent in agent_results:
257
+ if agent.visual_evidence is not None:
258
+ if isinstance(agent.visual_evidence, Image.Image):
259
+ return agent.visual_evidence
260
+ return None
261
+
262
+
263
+ def create_fft_display(agent_results: List[AgentEvidence]) -> go.Figure:
264
+ """Create FFT magnitude spectrum heatmap."""
265
+ for agent in agent_results:
266
+ if agent.agent_name == "Generative Model Agent":
267
+ for sf in agent.sub_findings:
268
+ if "magnitude_spectrum" in sf:
269
+ mag = sf["magnitude_spectrum"]
270
+ fig = go.Figure(data=go.Heatmap(
271
+ z=mag,
272
+ colorscale="Viridis",
273
+ showscale=True,
274
+ colorbar=dict(title="Log Magnitude"),
275
+ ))
276
+ fig.update_layout(
277
+ title="2D FFT Magnitude Spectrum",
278
+ height=400,
279
+ margin=dict(l=40, r=40, t=50, b=40),
280
+ paper_bgcolor="rgba(0,0,0,0)",
281
+ xaxis=dict(showticklabels=False),
282
+ yaxis=dict(showticklabels=False, scaleanchor="x"),
283
+ )
284
+ return fig
285
+ # Return empty figure
286
+ fig = go.Figure()
287
+ fig.update_layout(height=400, title="FFT Spectrum (not available)")
288
+ return fig
289
+
290
+
291
+ def create_noise_map_display(agent_results: List[AgentEvidence]) -> go.Figure:
292
+ """Create noise residual heatmap."""
293
+ for agent in agent_results:
294
+ if agent.agent_name == "Sensor Characteristics Agent":
295
+ for sf in agent.sub_findings:
296
+ if "noise_map" in sf:
297
+ nm = sf["noise_map"]
298
+ fig = go.Figure(data=go.Heatmap(
299
+ z=nm,
300
+ colorscale="Hot",
301
+ showscale=True,
302
+ colorbar=dict(title="Noise Energy"),
303
+ ))
304
+ fig.update_layout(
305
+ title="PRNU Noise Residual Map",
306
+ height=400,
307
+ margin=dict(l=40, r=40, t=50, b=40),
308
+ paper_bgcolor="rgba(0,0,0,0)",
309
+ xaxis=dict(showticklabels=False),
310
+ yaxis=dict(showticklabels=False, scaleanchor="x"),
311
+ )
312
+ return fig
313
+ fig = go.Figure()
314
+ fig.update_layout(height=400, title="Noise Map (not available)")
315
+ return fig
316
+
317
+
318
+ def create_benford_chart(agent_results: List[AgentEvidence]) -> go.Figure:
319
+ """Create Benford's Law comparison chart."""
320
+ for agent in agent_results:
321
+ if agent.agent_name == "Statistical Priors Agent":
322
+ for sf in agent.sub_findings:
323
+ if "observed" in sf and "benford_expected" in sf:
324
+ observed = sf["observed"]
325
+ expected = sf["benford_expected"]
326
+ digits = list(range(1, 10))
327
+
328
+ fig = go.Figure()
329
+ fig.add_trace(go.Bar(
330
+ x=digits, y=expected,
331
+ name="Benford's Law (Expected)",
332
+ marker_color="rgba(100, 150, 255, 0.7)",
333
+ ))
334
+ fig.add_trace(go.Bar(
335
+ x=digits, y=observed,
336
+ name="Observed Distribution",
337
+ marker_color="rgba(255, 100, 100, 0.7)",
338
+ ))
339
+ fig.update_layout(
340
+ title=f"Benford's Law Analysis (χ²={sf.get('chi_squared', 0):.5f})",
341
+ xaxis=dict(title="First Digit", dtick=1),
342
+ yaxis=dict(title="Proportion"),
343
+ barmode="group",
344
+ height=350,
345
+ margin=dict(l=50, r=30, t=50, b=40),
346
+ paper_bgcolor="rgba(0,0,0,0)",
347
+ font={"family": "Inter, sans-serif"},
348
+ )
349
+ return fig
350
+ fig = go.Figure()
351
+ fig.update_layout(height=350, title="Benford's Law (not available)")
352
+ return fig
353
+
354
+
355
+ def format_metadata_table(agent_results: List[AgentEvidence]) -> list:
356
+ """Extract EXIF data as table rows."""
357
+ for agent in agent_results:
358
+ if agent.agent_name == "Metadata Agent":
359
+ for sf in agent.sub_findings:
360
+ if "exif_data" in sf:
361
+ rows = [[k, v[:100]] for k, v in sf["exif_data"].items()]
362
+ if not rows:
363
+ rows = [["(No EXIF data)", "Image has no metadata"]]
364
+ return rows
365
+ return [["(Not available)", ""]]
366
+
367
+
368
+ # ─── Main Analysis Pipeline ─────────────────────────────────────────
369
+
370
+ def analyze_image(img):
371
+ """Main entry point for Gradio β€” runs full FORENSIQ pipeline."""
372
+ if img is None:
373
+ return (
374
+ "<div style='text-align:center;padding:40px;color:#888;'>Upload an image to begin analysis</div>",
375
+ go.Figure(),
376
+ go.Figure(),
377
+ go.Figure(),
378
+ "Upload an image to begin analysis.",
379
+ "",
380
+ "",
381
+ go.Figure(),
382
+ None,
383
+ go.Figure(),
384
+ go.Figure(),
385
+ [["", ""]],
386
+ "",
387
+ )
388
+
389
+ try:
390
+ # Convert numpy array to PIL if needed
391
+ if isinstance(img, np.ndarray):
392
+ img = Image.fromarray(img)
393
+
394
+ agent_results, verdict, reasoning_tree_md = run_all_agents(img)
395
+
396
+ # Build verdict HTML
397
+ prob = verdict.probability_fake
398
+ if prob > 0.65:
399
+ bg = "linear-gradient(135deg, #dc3545, #c82333)"
400
+ icon = "πŸ”΄"
401
+ elif prob > 0.45:
402
+ bg = "linear-gradient(135deg, #fd7e14, #e8590c)"
403
+ icon = "🟠"
404
+ elif prob > 0.25:
405
+ bg = "linear-gradient(135deg, #ffc107, #e0a800)"
406
+ icon = "🟑"
407
+ else:
408
+ bg = "linear-gradient(135deg, #28a745, #218838)"
409
+ icon = "βœ…"
410
+
411
+ verdict_html = f"""
412
+ <div style="background:{bg}; color:white; padding:24px; border-radius:16px;
413
+ text-align:center; font-family:Inter,sans-serif; box-shadow: 0 4px 15px rgba(0,0,0,0.2);">
414
+ <div style="font-size:48px; margin-bottom:8px;">{icon}</div>
415
+ <div style="font-size:28px; font-weight:700; margin-bottom:4px;">{verdict.verdict}</div>
416
+ <div style="font-size:42px; font-weight:800;">{prob:.1%}</div>
417
+ <div style="font-size:14px; opacity:0.9; margin-top:4px;">
418
+ Confidence: {verdict.confidence} | Agents: {len([a for a in agent_results if a.failure_prob < 0.8])}/7 active
419
+ </div>
420
+ </div>
421
+ """
422
+
423
+ # Create all visualizations
424
+ gauge = create_gauge_chart(verdict.probability_fake, verdict.verdict)
425
+ radar = create_radar_chart(agent_results)
426
+ bar_chart = create_agent_bar_chart(agent_results)
427
+ ela_img = create_ela_display(agent_results)
428
+ fft_fig = create_fft_display(agent_results)
429
+ noise_fig = create_noise_map_display(agent_results)
430
+ benford_fig = create_benford_chart(agent_results)
431
+ metadata_rows = format_metadata_table(agent_results)
432
+
433
+ return (
434
+ verdict_html,
435
+ gauge,
436
+ radar,
437
+ bar_chart,
438
+ verdict.forensic_report,
439
+ reasoning_tree_md,
440
+ verdict.court_brief,
441
+ fft_fig,
442
+ ela_img,
443
+ noise_fig,
444
+ benford_fig,
445
+ metadata_rows,
446
+ _build_agent_details_md(agent_results),
447
+ )
448
+ except Exception as e:
449
+ error_html = f"""
450
+ <div style="background:linear-gradient(135deg, #6c757d, #495057); color:white;
451
+ padding:24px; border-radius:16px; text-align:center;">
452
+ <div style="font-size:48px;">⚠️</div>
453
+ <div style="font-size:20px; font-weight:700;">Analysis Error</div>
454
+ <div style="font-size:14px; margin-top:8px;">{str(e)}</div>
455
+ </div>
456
+ """
457
+ empty_fig = go.Figure()
458
+ return (
459
+ error_html,
460
+ empty_fig, empty_fig, empty_fig,
461
+ f"Error during analysis: {str(e)}", "", "",
462
+ empty_fig, None, empty_fig, empty_fig,
463
+ [["Error", str(e)]],
464
+ "",
465
+ )
466
+
467
+
468
+ def _build_agent_details_md(agent_results: List[AgentEvidence]) -> str:
469
+ """Build detailed agent findings markdown."""
470
+ md = ""
471
+ for agent in agent_results:
472
+ if agent.violation_score > 0.2:
473
+ badge = "πŸ”΄ VIOLATED"
474
+ elif agent.violation_score < -0.1:
475
+ badge = "🟒 COMPLIANT"
476
+ elif agent.failure_prob > 0.7:
477
+ badge = "βšͺ SKIPPED"
478
+ else:
479
+ badge = "🟑 NEUTRAL"
480
+
481
+ md += f"### {agent.agent_name} β€” {badge}\n\n"
482
+ md += f"**Score:** {agent.violation_score:+.3f} | "
483
+ md += f"**Confidence:** {agent.confidence:.1%} | "
484
+ md += f"**Failure:** {agent.failure_prob:.1%}\n\n"
485
+
486
+ for sf in agent.sub_findings:
487
+ test = sf.get("test", "")
488
+ note = sf.get("note", "")
489
+ s = sf.get("score", 0)
490
+ ic = "πŸ”΄" if s > 0.2 else "🟒" if s < -0.1 else "🟑"
491
+ md += f"- {ic} **{test}** ({s:+.2f}): {note}\n"
492
+ md += "\n---\n\n"
493
+ return md
494
+
495
+
496
+ # ─── Gradio UI ───────────────────────────────────────────────────────
497
+
498
+ CUSTOM_CSS = """
499
+ .gradio-container {
500
+ max-width: 1400px !important;
501
+ font-family: 'Inter', sans-serif !important;
502
+ }
503
+ .main-title {
504
+ text-align: center;
505
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
506
+ -webkit-background-clip: text;
507
+ -webkit-text-fill-color: transparent;
508
+ font-size: 2.5em !important;
509
+ font-weight: 800 !important;
510
+ margin-bottom: 0 !important;
511
+ }
512
+ .subtitle {
513
+ text-align: center;
514
+ color: #6c757d;
515
+ font-size: 1.1em;
516
+ margin-top: 0;
517
+ }
518
+ .tab-content {
519
+ padding: 10px;
520
+ }
521
+ footer { display: none !important; }
522
+ """
523
+
524
+ HEADER_MD = """
525
+ <div style="text-align:center; padding: 10px 0;">
526
+ <h1 class="main-title">πŸ”¬ FORENSIQ</h1>
527
+ <p class="subtitle">Physics-Based, Multi-Agent Forensic Framework for Explainable Deepfake Detection</p>
528
+ <p style="color:#888; font-size:0.85em;">
529
+ 7 Independent Forensic Agents β€’ Bayesian Evidence Synthesis β€’ Court-Admissible Reports
530
+ </p>
531
+ </div>
532
+ """
533
+
534
+ def build_app():
535
+ with gr.Blocks(
536
+ title="FORENSIQ β€” Deepfake Detection",
537
+ theme=gr.themes.Soft(
538
+ primary_hue="purple",
539
+ secondary_hue="blue",
540
+ ),
541
+ css=CUSTOM_CSS,
542
+ ) as demo:
543
+ gr.HTML(HEADER_MD)
544
+
545
+ with gr.Row(equal_height=False):
546
+ # Left column: input
547
+ with gr.Column(scale=1, min_width=300):
548
+ image_input = gr.Image(
549
+ label="πŸ“· Upload Suspect Image",
550
+ type="pil",
551
+ height=350,
552
+ sources=["upload", "clipboard"],
553
+ )
554
+ analyze_btn = gr.Button(
555
+ "πŸ”¬ Run Forensic Analysis",
556
+ variant="primary",
557
+ size="lg",
558
+ )
559
+ gr.Markdown("""
560
+ <div style="font-size:0.8em; color:#888; padding:8px;">
561
+ <b>Supported:</b> JPEG, PNG, WebP, BMP, TIFF<br>
562
+ <b>Agents:</b> Optical Physics β€’ Sensor β€’ Generative Model β€’ Statistical β€’ Semantic β€’ Metadata β€’ Text<br>
563
+ <b>Engine:</b> Bayesian Evidence Synthesis with Independence Correction
564
+ </div>
565
+ """)
566
+
567
+ # Right column: verdict
568
+ with gr.Column(scale=1, min_width=300):
569
+ verdict_html = gr.HTML(
570
+ value="<div style='text-align:center;padding:60px;color:#aaa;font-size:1.2em;'>Upload an image and click Analyze</div>"
571
+ )
572
+ gauge_plot = gr.Plot(label="Confidence Gauge")
573
+
574
+ # Tabs for detailed results
575
+ with gr.Tabs():
576
+ with gr.Tab("πŸ“Š Overview"):
577
+ with gr.Row():
578
+ radar_plot = gr.Plot(label="Agent Scores Radar")
579
+ bar_plot = gr.Plot(label="Agent Violation Scores")
580
+ agent_details_md = gr.Markdown(label="Agent Details")
581
+
582
+ with gr.Tab("πŸ”Š Frequency Analysis"):
583
+ with gr.Row():
584
+ fft_plot = gr.Plot(label="FFT Magnitude Spectrum")
585
+ benford_plot = gr.Plot(label="Benford's Law Analysis")
586
+
587
+ with gr.Tab("πŸ”¬ Signal Forensics"):
588
+ with gr.Row():
589
+ noise_plot = gr.Plot(label="PRNU Noise Residual Map")
590
+ ela_image = gr.Image(label="Error Level Analysis (ELA)", type="pil")
591
+
592
+ with gr.Tab("πŸ“‹ Metadata"):
593
+ metadata_table = gr.Dataframe(
594
+ headers=["Field", "Value"],
595
+ label="EXIF Metadata",
596
+ wrap=True,
597
+ )
598
+
599
+ with gr.Tab("πŸ“„ Forensic Report"):
600
+ report_md = gr.Markdown(label="Full Forensic Report")
601
+
602
+ with gr.Tab("🌳 Reasoning Tree"):
603
+ tree_md = gr.Markdown(label="Reasoning Tree")
604
+
605
+ with gr.Tab("βš–οΈ Court Brief"):
606
+ court_md = gr.Markdown(label="Court Brief (FRE 702)")
607
+
608
+ # Wire up the analysis
609
+ analyze_btn.click(
610
+ fn=analyze_image,
611
+ inputs=[image_input],
612
+ outputs=[
613
+ verdict_html,
614
+ gauge_plot,
615
+ radar_plot,
616
+ bar_plot,
617
+ report_md,
618
+ tree_md,
619
+ court_md,
620
+ fft_plot,
621
+ ela_image,
622
+ noise_plot,
623
+ benford_plot,
624
+ metadata_table,
625
+ agent_details_md,
626
+ ],
627
+ )
628
+
629
+ # Footer
630
+ gr.HTML("""
631
+ <div style="text-align:center; padding:20px; color:#aaa; font-size:0.8em; border-top:1px solid #eee; margin-top:20px;">
632
+ <b>FORENSIQ v1.0</b> β€” Physics-Based Multi-Agent Forensic Framework<br>
633
+ 7 Agents β€’ 127+ Physical Constraints β€’ Bayesian Evidence Synthesis β€’ Court-Admissible Reports<br>
634
+ Powered by Qwen2.5-VL for semantic analysis β€’ Signal processing via NumPy/SciPy
635
+ </div>
636
+ """)
637
+
638
+ return demo
639
+
640
+
641
+ if __name__ == "__main__":
642
+ demo = build_app()
643
+ demo.launch(server_name="0.0.0.0", server_port=7860)