KoreaPeter commited on
Commit
2cd26aa
Β·
verified Β·
1 Parent(s): 46ab4de

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +314 -158
app.py CHANGED
@@ -1,13 +1,8 @@
1
  import gradio as gr
2
  import torch
3
- import matplotlib
4
- matplotlib.use("Agg")
5
- import matplotlib.pyplot as plt
6
- import matplotlib.patches as mpatches
7
- import numpy as np
8
  from transformers import pipeline
9
 
10
- # ── Model registry ───────────────────────────────────────────────────────────
11
  MODELS = {
12
  ("fast", "kodf"): "KoreaPeter/ms-eff-gcvit-deepfake-b0-kodf",
13
  ("fast", "celeb-df-v2"): "KoreaPeter/ms-eff-gcvit-deepfake-b0-celeb-df-v2",
@@ -32,81 +27,14 @@ def _get_pipeline(variant: str, dataset: str):
32
  return _cache[key]
33
 
34
 
35
- # ── Frame-level graph ─────────────────────────────────────────────────────────
36
- def _build_frame_graph(frame_scores: list, final_fake_prob: float):
37
- scores = [round(float(s), 4) for s in frame_scores]
38
- frames = list(range(1, len(scores) + 1))
39
-
40
- colors = []
41
- for s in scores:
42
- if s >= 0.7:
43
- colors.append("#FF4B4B") # red β€” FAKE
44
- elif s >= 0.4:
45
- colors.append("#FFD700") # yellow β€” UNCERTAIN
46
- else:
47
- colors.append("#39FF14") # green β€” REAL
48
-
49
- fig, ax = plt.subplots(figsize=(10, 4))
50
- fig.patch.set_facecolor("#111111")
51
- ax.set_facecolor("#1a1a1a")
52
-
53
- # μ˜μ—­ 색칠
54
- ax.axhspan(0.7, 1.0, color="#FF4B4B", alpha=0.08)
55
- ax.axhspan(0.4, 0.7, color="#FFD700", alpha=0.08)
56
- ax.axhspan(0.0, 0.4, color="#39FF14", alpha=0.08)
57
-
58
- # κΈ°μ€€μ„ 
59
- ax.axhline(0.7, color="#FF4B4B", linewidth=0.8, linestyle="--", alpha=0.5)
60
- ax.axhline(0.4, color="#FFD700", linewidth=0.8, linestyle="--", alpha=0.5)
61
-
62
- # μ΅œμ’… μŠ€μ½”μ–΄ μˆ˜ν‰μ„ 
63
- ax.axhline(final_fake_prob, color="#ffffff", linewidth=1.2,
64
- linestyle=":", alpha=0.6, label=f"Final score: {final_fake_prob:.4f}")
65
-
66
- # λ°” 차트
67
- ax.bar(frames, scores, color=colors, width=0.6, alpha=0.85, zorder=3)
68
-
69
- # μ„  μ—°κ²°
70
- ax.plot(frames, scores, color="#aaaaaa", linewidth=1.0,
71
- marker="o", markersize=4, alpha=0.7, zorder=4)
72
-
73
- # μΆ• μŠ€νƒ€μΌ
74
- ax.set_xlim(0.3, len(frames) + 0.7)
75
- ax.set_ylim(0.0, 1.05)
76
- ax.set_xlabel("Frame", color="#aaaaaa", fontsize=11)
77
- ax.set_ylabel("FAKE Probability", color="#aaaaaa", fontsize=11)
78
- ax.set_title("Frame-level FAKE Probability", color="#e5e5e5", fontsize=13, pad=10)
79
- ax.tick_params(colors="#aaaaaa")
80
- for spine in ax.spines.values():
81
- spine.set_edgecolor("#333333")
82
-
83
- # λ²”λ‘€
84
- patches = [
85
- mpatches.Patch(color="#FF4B4B", alpha=0.7, label="FAKE (β‰₯ 0.7)"),
86
- mpatches.Patch(color="#FFD700", alpha=0.7, label="UNCERTAIN (0.4–0.7)"),
87
- mpatches.Patch(color="#39FF14", alpha=0.7, label="REAL (< 0.4)"),
88
- ]
89
- ax.legend(handles=patches, facecolor="#1a1a1a", edgecolor="#333333",
90
- labelcolor="#e5e5e5", fontsize=9, loc="upper right")
91
-
92
- plt.tight_layout()
93
- return fig
94
-
95
-
96
- # ── Inference ─────────────────────────────────────────────────────────────────
97
  def predict(
98
- video_path: str,
99
- variant: str,
100
- dataset: str,
101
- num_frames: int,
102
- margin_ratio: float,
103
- conf_thres: float,
104
- min_face_ratio: float,
105
- tta_hflip: float,
106
- agg_mode: str,
107
  ):
108
  if video_path is None:
109
- return None, "⚠️ Please upload a video.", None
110
 
111
  try:
112
  pipe = _get_pipeline(variant, dataset)
@@ -121,136 +49,357 @@ def predict(
121
  return_frame_scores=True,
122
  )
123
  except Exception as e:
124
- return None, f"❌ Pipeline error: {e}", None
125
 
126
- # no_face
127
  if results[0]["label"] == "no_face_detected":
128
- return (
129
- {"REAL": 0.0, "FAKE": 0.0},
130
- "😢 No face detected.\nPlease use a video with a clearly visible face.",
131
- None,
132
- )
133
 
134
  scores = {r["label"].upper(): round(r["score"], 4)
135
  for r in results if r.get("label") in ("fake", "real")}
136
-
137
  fake_prob = scores.get("FAKE", 0.0)
138
 
139
  if fake_prob >= 0.7:
140
- verdict = f"πŸ”΄ FAKE ({fake_prob * 100:.1f}% confidence)"
141
  elif fake_prob >= 0.4:
142
- verdict = f"🟑 UNCERTAIN (FAKE {fake_prob * 100:.1f}%)"
143
  else:
144
- verdict = f"🟒 REAL ({(1 - fake_prob) * 100:.1f}% confidence)"
145
 
146
  frame_scores = next(
147
  (r["frame_scores"] for r in results if "frame_scores" in r), None
148
  )
149
 
150
- fig = _build_frame_graph(frame_scores, fake_prob) if frame_scores else None
151
-
152
- return scores, verdict, fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
 
155
- # ── Theme & CSS ───────────────────────────────────────────────────────────────
156
  THEME = gr.themes.Base(
157
- primary_hue="green",
158
  neutral_hue="slate",
159
  font=gr.themes.GoogleFont("Inter"),
160
  ).set(
161
- body_background_fill="#0a0a0a",
162
- body_text_color="#e5e5e5",
163
- block_background_fill="#111111",
164
- block_border_color="#222222",
165
- input_background_fill="#0d0d0d",
166
- button_primary_background_fill="#1a2c50",
167
- button_primary_text_color="#39FF14",
168
- button_primary_border_color="#39FF14",
169
- button_primary_background_fill_hover="#39FF14",
170
- button_primary_text_color_hover="#000000",
 
 
 
 
 
 
 
 
171
  )
172
 
173
  CSS = """
174
- .result-box textarea { font-size: 1.1rem !important; font-weight: bold !important; text-align: center; }
 
175
  footer { display: none !important; }
176
- """
177
 
178
- # ── Build UI ──────────────────────────────────────────────────────────────────
179
- with gr.Blocks(theme=THEME, css=CSS, title="DeepGuard Video") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- gr.Markdown("""
182
- # 🎬 DeepGuard β€” Video Deepfake Detection
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
- MS-EffGCViT-based deepfake detection demo for videos Β· Frame-level inference with aggregation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- πŸ”— [GitHub](https://github.com/HanMoonSub/DeepGuard) Β· πŸ€— [HuggingFace](https://huggingface.co/KoreaPeter)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  """)
188
 
189
- with gr.Row():
190
- # ── Left: Model + Video ──────────────────────────────────────────────
191
  with gr.Column(scale=1, min_width=300):
192
  video_input = gr.Video(label="Upload Video")
193
 
194
- gr.Markdown("### ⚑ Model Variant")
195
  variant_radio = gr.Radio(
196
  choices=["fast", "pro"],
197
  value="fast",
198
  label=None,
199
- info="Fast (b0): 8.7M params | Pro (b5): 50.3M params",
 
200
  )
201
 
202
- gr.Markdown("### 🌍 Training Domain")
203
  dataset_radio = gr.Radio(
204
  choices=[
205
- ("πŸ‡°πŸ‡· KoDF β€” East Asian faces", "kodf"),
206
- ("🌟 Celeb-DF-v2 β€” Celebrity faces", "celeb-df-v2"),
207
- ("🌍 FF++ β€” Western faces", "ff++"),
208
  ],
209
  value="kodf",
210
  label=None,
 
211
  )
212
 
213
- run_btn = gr.Button("πŸ” Analyze", variant="primary", size="lg")
 
 
 
 
 
214
 
215
- # ── Right: Result ─────────────────────────────────────────────────────
216
  with gr.Column(scale=1, min_width=300):
217
- verdict_output = gr.Textbox(
218
- label="Verdict",
219
- interactive=False,
220
- lines=2,
221
- elem_classes=["result-box"],
222
  )
223
- score_output = gr.Label(label="Class Probabilities", num_top_classes=2)
224
-
225
- gr.Markdown("""
226
- ---
227
- **Model Guide**
228
-
229
- | | ⚑ Fast (b0) | πŸ”₯ Pro (b5) |
230
- |---|---|---|
231
- | **Params** | 8.7M | 50.3M |
232
- | **Resolution** | 224Γ—224 | 384Γ—384 |
233
- | **Speed** | Faster | Slower |
234
- | **Accuracy** | High | Higher |
235
-
236
- **Domain Guide**
237
-
238
- | Domain | Training Data | Best For |
239
- |---|---|---|
240
- | KoDF | 62K real / 175K fake (Korean) | East Asian faces |
241
- | Celeb-DF-v2 | 890 real / 5.6K fake (Celebrities) | Celebrity faces |
242
- | FF++ | 1K real / 6K fake (4 manipulation types) | Western faces |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  """)
244
 
245
- # ── Advanced Settings ─────────────────────────────────────────────────────
246
- with gr.Accordion("βš™οΈ Advanced Settings", open=True):
247
- gr.Markdown("Adjust inference parameters. Defaults are recommended for most cases.")
248
  with gr.Row():
249
  with gr.Column():
250
  num_frames_slider = gr.Slider(
251
  minimum=5, maximum=60, value=20, step=1,
252
  label="Num Frames",
253
- info="Number of frames sampled from the video",
254
  )
255
  margin_ratio_slider = gr.Slider(
256
  minimum=0.0, maximum=0.5, value=0.2, step=0.05,
@@ -260,7 +409,7 @@ MS-EffGCViT-based deepfake detection demo for videos Β· Frame-level inference wi
260
  tta_hflip_slider = gr.Slider(
261
  minimum=0.0, maximum=1.0, value=0.0, step=0.1,
262
  label="TTA Horizontal Flip",
263
- info="Probability of horizontal flip for test-time augmentation",
264
  )
265
  with gr.Column():
266
  agg_radio = gr.Radio(
@@ -277,14 +426,21 @@ MS-EffGCViT-based deepfake detection demo for videos Β· Frame-level inference wi
277
  min_face_ratio_slider = gr.Slider(
278
  minimum=0.001, maximum=0.1, value=0.01, step=0.001,
279
  label="Min Face Ratio",
280
- info="Minimum face-to-frame area ratio to process",
281
  )
282
 
283
- # ── Frame-level Graph ─────────────────────────────────────────────────────
284
- with gr.Accordion("πŸ“Š Frame-level Results", open=True):
285
- frame_graph = gr.Plot(label="Frame-level FAKE Probability")
 
 
 
 
 
 
 
286
 
287
- # ── Event bindings ────────────────────────────────────────────────────────
288
  run_btn.click(
289
  fn=predict,
290
  inputs=[
@@ -292,7 +448,7 @@ MS-EffGCViT-based deepfake detection demo for videos Β· Frame-level inference wi
292
  num_frames_slider, margin_ratio_slider, conf_thres_slider,
293
  min_face_ratio_slider, tta_hflip_slider, agg_radio,
294
  ],
295
- outputs=[score_output, verdict_output, frame_graph],
296
  )
297
 
298
  if __name__ == "__main__":
 
1
  import gradio as gr
2
  import torch
 
 
 
 
 
3
  from transformers import pipeline
4
 
5
+ # ── Model registry ────────────────────────────────────────────────────────────
6
  MODELS = {
7
  ("fast", "kodf"): "KoreaPeter/ms-eff-gcvit-deepfake-b0-kodf",
8
  ("fast", "celeb-df-v2"): "KoreaPeter/ms-eff-gcvit-deepfake-b0-celeb-df-v2",
 
27
  return _cache[key]
28
 
29
 
30
+ # ── Inference ──────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  def predict(
32
+ video_path, variant, dataset,
33
+ num_frames, margin_ratio, conf_thres,
34
+ min_face_ratio, tta_hflip, agg_mode,
 
 
 
 
 
 
35
  ):
36
  if video_path is None:
37
+ return None, _verdict_html("warning"), None
38
 
39
  try:
40
  pipe = _get_pipeline(variant, dataset)
 
49
  return_frame_scores=True,
50
  )
51
  except Exception as e:
52
+ return None, _verdict_html("error", str(e)), None
53
 
 
54
  if results[0]["label"] == "no_face_detected":
55
+ return {"REAL": 0.0, "FAKE": 0.0}, _verdict_html("no_face"), None
 
 
 
 
56
 
57
  scores = {r["label"].upper(): round(r["score"], 4)
58
  for r in results if r.get("label") in ("fake", "real")}
 
59
  fake_prob = scores.get("FAKE", 0.0)
60
 
61
  if fake_prob >= 0.7:
62
+ verdict_html = _verdict_html("fake", fake_prob)
63
  elif fake_prob >= 0.4:
64
+ verdict_html = _verdict_html("uncertain", fake_prob)
65
  else:
66
+ verdict_html = _verdict_html("real", fake_prob)
67
 
68
  frame_scores = next(
69
  (r["frame_scores"] for r in results if "frame_scores" in r), None
70
  )
71
 
72
+ table_data = None
73
+ if frame_scores:
74
+ table_data = []
75
+ for i, s in enumerate(frame_scores, 1):
76
+ s = round(float(s), 4)
77
+ if s >= 0.7:
78
+ tag = "πŸ”΄ FAKE"
79
+ elif s >= 0.4:
80
+ tag = "🟑 UNCERTAIN"
81
+ else:
82
+ tag = "🟒 REAL"
83
+ table_data.append([i, f"{s:.4f}", f"{s * 100:.1f}%", tag])
84
+
85
+ return scores, verdict_html, table_data
86
+
87
+
88
+ def _verdict_html(state: str, value=None) -> str:
89
+ templates = {
90
+ "warning": (
91
+ "#fbbf24", "#1c1400",
92
+ "⚠️", "No Video Uploaded",
93
+ "Please upload a video file to begin analysis."
94
+ ),
95
+ "error": (
96
+ "#f87171", "#1c0000",
97
+ "❌", "Pipeline Error",
98
+ str(value)
99
+ ),
100
+ "no_face": (
101
+ "#94a3b8", "#0f172a",
102
+ "😢", "No Face Detected",
103
+ "Please use a video with a clearly visible face."
104
+ ),
105
+ "fake": (
106
+ "#f87171", "#1c0000",
107
+ "πŸ”΄", "FAKE DETECTED",
108
+ f"Confidence: {float(value) * 100:.1f}%"
109
+ ),
110
+ "uncertain": (
111
+ "#fbbf24", "#1c1400",
112
+ "🟑", "UNCERTAIN",
113
+ f"FAKE probability: {float(value) * 100:.1f}%"
114
+ ),
115
+ "real": (
116
+ "#4ade80", "#001c09",
117
+ "🟒", "REAL",
118
+ f"Confidence: {(1 - float(value)) * 100:.1f}%"
119
+ ),
120
+ }
121
+ color, bg, icon, title, sub = templates[state]
122
+ return f"""
123
+ <div style="
124
+ background: {bg};
125
+ border: 1.5px solid {color}44;
126
+ border-left: 4px solid {color};
127
+ border-radius: 12px;
128
+ padding: 20px 24px;
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 16px;
132
+ font-family: 'Inter', sans-serif;
133
+ ">
134
+ <span style="font-size: 2.2rem; line-height: 1;">{icon}</span>
135
+ <div>
136
+ <div style="color: {color}; font-size: 1.25rem; font-weight: 700; letter-spacing: 0.05em;">{title}</div>
137
+ <div style="color: #94a3b8; font-size: 0.9rem; margin-top: 2px;">{sub}</div>
138
+ </div>
139
+ </div>
140
+ """
141
 
142
 
143
+ # ── Theme ──────────────────────────────────────────────────────────────────────
144
  THEME = gr.themes.Base(
145
+ primary_hue="violet",
146
  neutral_hue="slate",
147
  font=gr.themes.GoogleFont("Inter"),
148
  ).set(
149
+ body_background_fill="#0b0d17",
150
+ body_text_color="#e2e8f0",
151
+ block_background_fill="#111827",
152
+ block_border_color="#1e293b",
153
+ block_border_width="1px",
154
+ block_radius="12px",
155
+ block_label_text_color="#64748b",
156
+ block_label_text_size="sm",
157
+ input_background_fill="#0f172a",
158
+ input_border_color="#1e293b",
159
+ input_border_color_focus="#7c3aed",
160
+ button_primary_background_fill="linear-gradient(135deg, #6d28d9, #4f46e5)",
161
+ button_primary_background_fill_hover="linear-gradient(135deg, #7c3aed, #6366f1)",
162
+ button_primary_text_color="#ffffff",
163
+ button_primary_border_color="transparent",
164
+ button_secondary_background_fill="#1e293b",
165
+ button_secondary_text_color="#e2e8f0",
166
+ slider_color="#7c3aed",
167
  )
168
 
169
  CSS = """
170
+ /* ── Global ── */
171
+ body { background: #0b0d17 !important; }
172
  footer { display: none !important; }
 
173
 
174
+ /* ── Hero banner ── */
175
+ .hero-banner {
176
+ background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
177
+ border-radius: 16px;
178
+ padding: 36px 40px;
179
+ margin-bottom: 8px;
180
+ border: 1px solid #312e81;
181
+ position: relative;
182
+ overflow: hidden;
183
+ }
184
+ .hero-banner::before {
185
+ content: '';
186
+ position: absolute;
187
+ top: -60px; right: -60px;
188
+ width: 220px; height: 220px;
189
+ background: radial-gradient(circle, #7c3aed33, transparent 70%);
190
+ border-radius: 50%;
191
+ }
192
+ .hero-title {
193
+ font-size: 2rem;
194
+ font-weight: 800;
195
+ background: linear-gradient(90deg, #a78bfa, #818cf8, #38bdf8);
196
+ -webkit-background-clip: text;
197
+ -webkit-text-fill-color: transparent;
198
+ background-clip: text;
199
+ margin: 0 0 6px 0;
200
+ line-height: 1.2;
201
+ }
202
+ .hero-sub {
203
+ color: #94a3b8;
204
+ font-size: 0.95rem;
205
+ margin: 0 0 16px 0;
206
+ }
207
+ .badge-row { display: flex; gap: 8px; flex-wrap: wrap; }
208
+ .badge {
209
+ background: #1e1b4b;
210
+ border: 1px solid #4338ca44;
211
+ color: #a5b4fc;
212
+ font-size: 0.75rem;
213
+ font-weight: 600;
214
+ padding: 4px 10px;
215
+ border-radius: 999px;
216
+ letter-spacing: 0.03em;
217
+ }
218
+ .badge-green { background: #052e16; border-color: #16a34a44; color: #4ade80; }
219
+ .badge-blue { background: #0c1a2e; border-color: #2563eb44; color: #60a5fa; }
220
+
221
+ /* ── Section label ── */
222
+ .section-label {
223
+ font-size: 0.7rem;
224
+ font-weight: 700;
225
+ letter-spacing: 0.12em;
226
+ text-transform: uppercase;
227
+ color: #475569;
228
+ margin: 0 0 8px 2px;
229
+ }
230
 
231
+ /* ── Pills (radio) ── */
232
+ .radio-group .wrap { gap: 8px !important; }
233
+ .radio-group label {
234
+ border: 1px solid #1e293b !important;
235
+ border-radius: 8px !important;
236
+ padding: 8px 16px !important;
237
+ background: #0f172a !important;
238
+ transition: all 0.15s ease;
239
+ cursor: pointer;
240
+ }
241
+ .radio-group label:has(input:checked) {
242
+ border-color: #7c3aed !important;
243
+ background: #1e1b4b !important;
244
+ color: #a78bfa !important;
245
+ }
246
 
247
+ /* ── Analyze button ── */
248
+ .analyze-btn {
249
+ margin-top: 12px !important;
250
+ height: 52px !important;
251
+ font-size: 1rem !important;
252
+ font-weight: 700 !important;
253
+ letter-spacing: 0.04em !important;
254
+ border-radius: 10px !important;
255
+ box-shadow: 0 0 24px #7c3aed44 !important;
256
+ transition: box-shadow 0.2s ease !important;
257
+ }
258
+ .analyze-btn:hover {
259
+ box-shadow: 0 0 36px #7c3aed88 !important;
260
+ }
261
+
262
+ /* ── Score label ── */
263
+ .score-label .label-wrap { gap: 10px !important; }
264
+ .score-label .label-item { border-radius: 8px !important; }
265
+
266
+ /* ── Dataframe ── */
267
+ .frame-table table { border-collapse: separate; border-spacing: 0; }
268
+ .frame-table thead th {
269
+ background: #1e293b !important;
270
+ color: #94a3b8 !important;
271
+ font-size: 0.75rem !important;
272
+ font-weight: 700 !important;
273
+ letter-spacing: 0.08em !important;
274
+ text-transform: uppercase;
275
+ padding: 10px 14px !important;
276
+ }
277
+ .frame-table tbody tr:hover td { background: #1e293b88 !important; }
278
+ .frame-table tbody td { padding: 9px 14px !important; font-size: 0.88rem !important; }
279
+
280
+ /* ── Accordion ── */
281
+ .gr-accordion { border-radius: 12px !important; border-color: #1e293b !important; }
282
+ """
283
 
284
+
285
+ # ── UI ─────────────────────────────────────────────────────────────────────────
286
+ with gr.Blocks(theme=THEME, css=CSS, title="DeepGuard") as demo:
287
+
288
+ # Hero
289
+ gr.HTML("""
290
+ <div class="hero-banner">
291
+ <p class="hero-title">🎬 DeepGuard</p>
292
+ <p class="hero-sub">MS-EffGCViT Β· Multi-Scale Efficient Global Context Vision Transformer</p>
293
+ <div class="badge-row">
294
+ <span class="badge">Video Deepfake Detection</span>
295
+ <span class="badge badge-green">Frame-level Inference</span>
296
+ <span class="badge badge-blue">6 Models Available</span>
297
+ <span class="badge">πŸ”— <a href="https://github.com/HanMoonSub/DeepGuard" target="_blank" style="color:inherit;text-decoration:none;">GitHub</a></span>
298
+ <span class="badge">πŸ€— <a href="https://huggingface.co/KoreaPeter" target="_blank" style="color:inherit;text-decoration:none;">HuggingFace</a></span>
299
+ </div>
300
+ </div>
301
  """)
302
 
303
+ with gr.Row(equal_height=False):
304
+ # ── Left panel ────────────────────────────────────────────────────────
305
  with gr.Column(scale=1, min_width=300):
306
  video_input = gr.Video(label="Upload Video")
307
 
308
+ gr.HTML('<p class="section-label">⚑ Model Variant</p>')
309
  variant_radio = gr.Radio(
310
  choices=["fast", "pro"],
311
  value="fast",
312
  label=None,
313
+ info="Fast (b0) Β· 8.7M params | Pro (b5) Β· 50.3M params",
314
+ elem_classes=["radio-group"],
315
  )
316
 
317
+ gr.HTML('<p class="section-label" style="margin-top:16px;">🌍 Training Domain</p>')
318
  dataset_radio = gr.Radio(
319
  choices=[
320
+ ("πŸ‡°πŸ‡· KoDF β€” East Asian faces", "kodf"),
321
+ ("🌟 Celeb-DF-v2 β€” Celebrity faces", "celeb-df-v2"),
322
+ ("🌍 FF++ β€” Western faces", "ff++"),
323
  ],
324
  value="kodf",
325
  label=None,
326
+ elem_classes=["radio-group"],
327
  )
328
 
329
+ run_btn = gr.Button(
330
+ "πŸ” Analyze Video",
331
+ variant="primary",
332
+ size="lg",
333
+ elem_classes=["analyze-btn"],
334
+ )
335
 
336
+ # ── Right panel ───────────────────────────────────────────────────────
337
  with gr.Column(scale=1, min_width=300):
338
+ verdict_output = gr.HTML(
339
+ value=_verdict_html("warning"),
340
+ label=None,
 
 
341
  )
342
+ score_output = gr.Label(
343
+ label="Class Probabilities",
344
+ num_top_classes=2,
345
+ elem_classes=["score-label"],
346
+ )
347
+
348
+ gr.HTML("""
349
+ <div style="
350
+ background:#0f172a;
351
+ border:1px solid #1e293b;
352
+ border-radius:12px;
353
+ padding:18px 20px;
354
+ margin-top:4px;
355
+ font-size:0.82rem;
356
+ color:#64748b;
357
+ line-height:1.8;
358
+ ">
359
+ <div style="color:#94a3b8;font-weight:700;font-size:0.78rem;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:10px;">Model Reference</div>
360
+ <table style="width:100%;border-collapse:collapse;color:#94a3b8;">
361
+ <thead>
362
+ <tr style="border-bottom:1px solid #1e293b;">
363
+ <th style="text-align:left;padding:4px 8px;color:#475569;font-size:0.72rem;letter-spacing:0.08em;">SPEC</th>
364
+ <th style="text-align:center;padding:4px 8px;color:#a78bfa;">⚑ Fast (b0)</th>
365
+ <th style="text-align:center;padding:4px 8px;color:#60a5fa;">πŸ”₯ Pro (b5)</th>
366
+ </tr>
367
+ </thead>
368
+ <tbody>
369
+ <tr><td style="padding:4px 8px;">Params</td><td style="text-align:center;">8.7M</td><td style="text-align:center;">50.3M</td></tr>
370
+ <tr><td style="padding:4px 8px;">Resolution</td><td style="text-align:center;">224Γ—224</td><td style="text-align:center;">384Γ—384</td></tr>
371
+ <tr><td style="padding:4px 8px;">Speed</td><td style="text-align:center;">Faster</td><td style="text-align:center;">Slower</td></tr>
372
+ <tr><td style="padding:4px 8px;">Accuracy</td><td style="text-align:center;">High</td><td style="text-align:center;">Higher</td></tr>
373
+ </tbody>
374
+ </table>
375
+ <div style="height:1px;background:#1e293b;margin:12px 0;"></div>
376
+ <div style="color:#94a3b8;font-weight:700;font-size:0.78rem;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:10px;">Domain Reference</div>
377
+ <table style="width:100%;border-collapse:collapse;color:#94a3b8;">
378
+ <thead>
379
+ <tr style="border-bottom:1px solid #1e293b;">
380
+ <th style="text-align:left;padding:4px 8px;color:#475569;font-size:0.72rem;letter-spacing:0.08em;">DOMAIN</th>
381
+ <th style="text-align:left;padding:4px 8px;color:#475569;font-size:0.72rem;letter-spacing:0.08em;">TRAINING DATA</th>
382
+ <th style="text-align:left;padding:4px 8px;color:#475569;font-size:0.72rem;letter-spacing:0.08em;">BEST FOR</th>
383
+ </tr>
384
+ </thead>
385
+ <tbody>
386
+ <tr><td style="padding:4px 8px;">πŸ‡°πŸ‡· KoDF</td><td style="padding:4px 8px;">62K real / 175K fake</td><td style="padding:4px 8px;">East Asian</td></tr>
387
+ <tr><td style="padding:4px 8px;">🌟 Celeb-DF-v2</td><td style="padding:4px 8px;">890 real / 5.6K fake</td><td style="padding:4px 8px;">Celebrity</td></tr>
388
+ <tr><td style="padding:4px 8px;">🌍 FF++</td><td style="padding:4px 8px;">1K real / 6K fake</td><td style="padding:4px 8px;">Western</td></tr>
389
+ </tbody>
390
+ </table>
391
+ </div>
392
  """)
393
 
394
+ # ── Advanced Settings ──────────────────────────────────────────────────────
395
+ with gr.Accordion("βš™οΈ Advanced Settings", open=False):
396
+ gr.HTML('<p style="color:#64748b;font-size:0.85rem;margin:0 0 12px 0;">Defaults are recommended for most cases.</p>')
397
  with gr.Row():
398
  with gr.Column():
399
  num_frames_slider = gr.Slider(
400
  minimum=5, maximum=60, value=20, step=1,
401
  label="Num Frames",
402
+ info="Frames sampled from the video",
403
  )
404
  margin_ratio_slider = gr.Slider(
405
  minimum=0.0, maximum=0.5, value=0.2, step=0.05,
 
409
  tta_hflip_slider = gr.Slider(
410
  minimum=0.0, maximum=1.0, value=0.0, step=0.1,
411
  label="TTA Horizontal Flip",
412
+ info="Probability of horizontal flip for TTA",
413
  )
414
  with gr.Column():
415
  agg_radio = gr.Radio(
 
426
  min_face_ratio_slider = gr.Slider(
427
  minimum=0.001, maximum=0.1, value=0.01, step=0.001,
428
  label="Min Face Ratio",
429
+ info="Minimum face-to-frame area ratio",
430
  )
431
 
432
+ # ── Frame-level Table ──────────────────────────────────────────────────────
433
+ with gr.Accordion("πŸ“Š Frame-level Results", open=True):
434
+ frame_table = gr.Dataframe(
435
+ headers=["Frame", "FAKE Score", "FAKE %", "Verdict"],
436
+ datatype=["number", "str", "str", "str"],
437
+ label=None,
438
+ interactive=False,
439
+ wrap=True,
440
+ elem_classes=["frame-table"],
441
+ )
442
 
443
+ # ── Bindings ───────────────────────────────────────────────────────────────
444
  run_btn.click(
445
  fn=predict,
446
  inputs=[
 
448
  num_frames_slider, margin_ratio_slider, conf_thres_slider,
449
  min_face_ratio_slider, tta_hflip_slider, agg_radio,
450
  ],
451
+ outputs=[score_output, verdict_output, frame_table],
452
  )
453
 
454
  if __name__ == "__main__":