seawolf2357 commited on
Commit
a4c6a53
·
verified ·
1 Parent(s): 528db87

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +628 -477
app.py CHANGED
@@ -1,212 +1,106 @@
1
- """
2
- ANSIM BLUR - Face Privacy Protection
3
- =====================================
4
- Advanced AI-Powered Face Detection & Privacy Protection
5
- Using YOLOv8 for face detection with Gaussian/Mosaic blur options
6
- """
7
-
8
  import gradio as gr
9
  import cv2
10
  import numpy as np
11
  import tempfile
12
  import os
13
  from pathlib import Path
14
- from typing import Optional, Tuple, List
15
  import torch
16
  from PIL import Image
17
 
18
- # ============================================
19
- # Constants & Configuration
20
- # ============================================
21
- BLUR_MODES = ["Gaussian Blur", "Mosaic Effect"]
22
-
23
- DEFAULT_CONFIG = {
24
- "confidence": 0.25,
25
- "iou": 0.45,
26
- "expand_ratio": 0.05,
27
- "blur_intensity": 51,
28
- "mosaic_size": 15,
29
- }
30
-
31
- SLIDER_CONFIG = {
32
- "confidence": {"minimum": 0.05, "maximum": 0.9, "step": 0.01},
33
- "iou": {"minimum": 0.1, "maximum": 0.9, "step": 0.01},
34
- "expand": {"minimum": 0.0, "maximum": 0.5, "step": 0.01},
35
- "blur": {"minimum": 15, "maximum": 151, "step": 2},
36
- "mosaic": {"minimum": 5, "maximum": 40, "step": 1},
37
- }
38
-
39
-
40
- # ============================================
41
- # Model Manager
42
- # ============================================
43
- class FaceDetector:
44
- """YOLOv8 Face Detection Model Manager"""
45
-
46
- def __init__(self, model_path: str = "yolov8-face-hf.pt"):
47
- self.model = None
48
- self.device = self._get_device()
49
- self._load_model(model_path)
50
-
51
- def _get_device(self) -> str:
52
- """Determine the best available device"""
53
  if torch.cuda.is_available():
54
- return "cuda"
55
  elif torch.backends.mps.is_available():
56
- return "mps"
57
- return "cpu"
58
-
59
- def _load_model(self, model_path: str) -> None:
60
- """Load YOLO model"""
61
- from ultralytics import YOLO
62
- self.model = YOLO(model_path)
63
- self.model.to(self.device)
64
-
65
- def detect(self, image: np.ndarray, conf: float, iou: float) -> List:
66
- """Run face detection on image"""
67
- with torch.no_grad():
68
- results = self.model.predict(
69
- image,
70
- conf=conf,
71
- iou=iou,
72
- verbose=False,
73
- device=self.device
74
- )
75
- return results
76
-
77
-
78
- # Initialize global detector
79
- detector = FaceDetector()
80
-
81
-
82
- # ============================================
83
- # Image Processing Functions
84
- # ============================================
85
- def ensure_odd(x: int) -> int:
86
- """Ensure kernel size is odd for OpenCV"""
87
  return x if x % 2 == 1 else x + 1
88
 
89
-
90
- def get_even_dimensions(w: int, h: int) -> Tuple[int, int]:
91
- """Ensure video dimensions are even for codec compatibility"""
92
  return (w if w % 2 == 0 else w - 1, h if h % 2 == 0 else h - 1)
93
 
94
-
95
- def apply_blur(
96
- face_roi: np.ndarray,
97
- mode: str,
98
- blur_kernel: int,
99
- mosaic_size: int = 15
100
- ) -> np.ndarray:
101
- """Apply blur or mosaic effect to face region"""
102
  if face_roi.size == 0:
103
  return face_roi
104
-
105
  if mode == "Gaussian Blur":
106
- k = ensure_odd(max(blur_kernel, 15))
107
  return cv2.GaussianBlur(face_roi, (k, k), 0)
108
- else: # Mosaic Effect
109
- m = max(2, mosaic_size)
110
  h, w = face_roi.shape[:2]
111
- small = cv2.resize(
112
- face_roi,
113
- (max(1, w // m), max(1, h // m)),
114
- interpolation=cv2.INTER_LINEAR
115
- )
116
- return cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
117
-
118
-
119
- def expand_bbox(
120
- x1: int, y1: int, x2: int, y2: int,
121
- expand_ratio: float,
122
- img_w: int, img_h: int
123
- ) -> Tuple[int, int, int, int]:
124
- """Expand bounding box by ratio and clip to image bounds"""
125
- if expand_ratio > 0:
126
- bw, bh = x2 - x1, y2 - y1
127
- dx, dy = int(bw * expand_ratio), int(bh * expand_ratio)
128
- x1, y1 = x1 - dx, y1 - dy
129
- x2, y2 = x2 + dx, y2 + dy
130
-
131
- # Clip to image bounds
132
- x1 = max(0, min(img_w, x1))
133
- x2 = max(0, min(img_w, x2))
134
- y1 = max(0, min(img_h, y1))
135
- y2 = max(0, min(img_h, y2))
136
-
137
- return x1, y1, x2, y2
138
-
139
-
140
- def blur_faces_in_image(
141
- image_bgr: np.ndarray,
142
- conf: float,
143
- iou: float,
144
- expand_ratio: float,
145
- mode: str,
146
- blur_kernel: int,
147
- mosaic_size: int
148
- ) -> Tuple[np.ndarray, int]:
149
- """Detect and blur faces in a single image"""
150
  h, w = image_bgr.shape[:2]
151
  face_count = 0
152
-
153
- results = detector.detect(image_bgr, conf, iou)
154
-
 
155
  for r in results:
156
  boxes = r.boxes.xyxy.cpu().numpy() if hasattr(r.boxes, "xyxy") else []
157
  face_count = len(boxes)
158
-
159
- for box in boxes:
160
- x1, y1, x2, y2 = map(int, box[:4])
161
- x1, y1, x2, y2 = expand_bbox(x1, y1, x2, y2, expand_ratio, w, h)
162
-
 
 
 
 
 
 
 
 
 
163
  if x2 <= x1 or y2 <= y1:
164
  continue
165
-
166
  roi = image_bgr[y1:y2, x1:x2]
167
- image_bgr[y1:y2, x1:x2] = apply_blur(roi, mode, blur_kernel, mosaic_size)
168
-
169
- return image_bgr, face_count
170
 
 
171
 
172
- def blur_faces_in_video(
173
- input_path: str,
174
- conf: float,
175
- iou: float,
176
- expand_ratio: float,
177
- mode: str,
178
- blur_kernel: int,
179
- mosaic_size: int,
180
- progress: gr.Progress
181
- ) -> Tuple[str, int, int]:
182
- """Process video file and blur all detected faces"""
183
  from moviepy.editor import VideoFileClip
184
 
185
  cap = cv2.VideoCapture(input_path)
186
  if not cap.isOpened():
187
- raise IOError("Cannot open video file")
188
-
189
- # Get video properties
190
  in_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
191
  in_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
192
  fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
193
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
194
-
195
- out_w, out_h = get_even_dimensions(in_w, in_h)
196
-
197
- # Create temp files
198
- temp_video = tempfile.NamedTemporaryFile(delete=False, suffix="_temp.mp4")
199
- output_video = tempfile.NamedTemporaryFile(delete=False, suffix="_blurred.mp4")
200
- temp_path = temp_video.name
201
- output_path = output_video.name
202
- temp_video.close()
203
- output_video.close()
204
-
205
- # Setup video writer
206
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
207
- writer = cv2.VideoWriter(temp_path, fourcc, fps, (out_w, out_h))
208
 
209
- frame_idx = 0
 
 
 
 
 
210
  total_faces = 0
211
 
212
  try:
@@ -214,43 +108,47 @@ def blur_faces_in_video(
214
  ret, frame = cap.read()
215
  if not ret:
216
  break
217
-
218
  frame = cv2.resize(frame, (out_w, out_h))
 
 
 
 
219
  h, w = frame.shape[:2]
220
-
221
- # Detect faces
222
- results = detector.detect(frame, conf, iou)
223
-
224
- if results:
225
- r = results[0]
226
- boxes = r.boxes.xyxy.cpu().numpy() if hasattr(r.boxes, "xyxy") else []
227
- total_faces += len(boxes)
228
-
229
- for box in boxes:
230
- x1, y1, x2, y2 = map(int, box[:4])
231
- x1, y1, x2, y2 = expand_bbox(x1, y1, x2, y2, expand_ratio, w, h)
232
-
233
- if x2 <= x1 or y2 <= y1:
234
- continue
235
-
236
- roi = frame[y1:y2, x1:x2]
237
- frame[y1:y2, x1:x2] = apply_blur(roi, mode, blur_kernel, mosaic_size)
238
-
239
- writer.write(frame)
240
- frame_idx += 1
241
-
242
- if total_frames > 0:
243
- progress(frame_idx / total_frames, desc=f"Processing frame {frame_idx}/{total_frames}")
244
-
 
 
245
  finally:
246
  cap.release()
247
- writer.release()
248
-
249
- # Merge audio from original video
250
  try:
251
  progress(0.95, desc="Merging audio...")
252
  original = VideoFileClip(input_path)
253
- processed = VideoFileClip(temp_path).set_audio(original.audio)
254
  processed.write_videofile(
255
  output_path,
256
  codec="libx264",
@@ -260,31 +158,16 @@ def blur_faces_in_video(
260
  )
261
  original.close()
262
  processed.close()
263
-
264
- # Clean up temp file
265
- if os.path.exists(temp_path):
266
- os.remove(temp_path)
267
-
268
- return output_path, total_faces, total_frames
269
-
270
  except Exception as e:
271
- print(f"Audio merging failed: {e}")
272
- return temp_path, total_faces, total_frames
273
 
274
 
275
- # ============================================
276
- # Gradio Processing Handlers
277
- # ============================================
278
- def process_image(
279
- image: Optional[Image.Image],
280
- conf: float,
281
- iou: float,
282
- expand_ratio: float,
283
- mode_choice: str,
284
- blur_intensity: int,
285
- mosaic_size: int
286
- ) -> Tuple[Optional[Image.Image], str]:
287
- """Main image processing handler"""
288
  if image is None:
289
  return None, "⚠️ Please upload an image first!"
290
 
@@ -292,12 +175,16 @@ def process_image(
292
  image_bgr = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
293
  h, w = image_bgr.shape[:2]
294
 
295
- # Set blur parameters based on mode
296
- blur_kernel = blur_intensity if mode_choice == "Gaussian Blur" else 51
297
- mosaic = mosaic_size if mode_choice == "Mosaic Effect" else 15
 
 
 
 
298
 
299
- # Process image
300
- result_bgr, face_count = blur_faces_in_image(
301
  image_bgr.copy(), conf, iou, expand_ratio,
302
  mode_choice, blur_kernel, mosaic
303
  )
@@ -307,7 +194,6 @@ def process_image(
307
  result_pil = Image.fromarray(result_rgb)
308
 
309
  # Generate log
310
- intensity_value = blur_intensity if mode_choice == "Gaussian Blur" else mosaic_size
311
  info_log = f"""✅ IMAGE PROCESSING COMPLETE!
312
  {'=' * 50}
313
  🖼️ Image Info:
@@ -321,7 +207,7 @@ def process_image(
321
  {'=' * 50}
322
  🎨 Blur Settings:
323
  • Style: {mode_choice}
324
- • Intensity: {intensity_value}
325
  {'=' * 50}
326
  👤 Results:
327
  • Faces Detected: {face_count}
@@ -332,31 +218,24 @@ def process_image(
332
  return result_pil, info_log
333
 
334
 
335
- def process_video(
336
- video: Optional[str],
337
- conf: float,
338
- iou: float,
339
- expand_ratio: float,
340
- mode_choice: str,
341
- blur_intensity: int,
342
- mosaic_size: int,
343
- progress: gr.Progress = gr.Progress()
344
- ) -> Tuple[Optional[str], str]:
345
- """Main video processing handler"""
346
  if video is None:
347
  return None, "⚠️ Please upload a video first!"
348
 
349
- # Set blur parameters based on mode
350
- blur_kernel = blur_intensity if mode_choice == "Gaussian Blur" else 51
351
- mosaic = mosaic_size if mode_choice == "Mosaic Effect" else 15
 
 
 
 
352
 
353
  try:
354
- output_path, total_faces, total_frames = blur_faces_in_video(
355
  video, conf, iou, expand_ratio,
356
  mode_choice, blur_kernel, mosaic, progress
357
  )
358
 
359
- intensity_value = blur_intensity if mode_choice == "Gaussian Blur" else mosaic_size
360
  info_log = f"""✅ VIDEO PROCESSING COMPLETE!
361
  {'=' * 50}
362
  🎥 Video Info:
@@ -370,7 +249,7 @@ def process_video(
370
  {'=' * 50}
371
  🎨 Blur Settings:
372
  • Style: {mode_choice}
373
- • Intensity: {intensity_value}
374
  {'=' * 50}
375
  👤 Results:
376
  • Total Faces Detected: {total_faces}
@@ -379,65 +258,102 @@ def process_video(
379
  💾 Ready to download!"""
380
 
381
  return output_path, info_log
382
-
383
  except Exception as e:
384
  return None, f"❌ Error: {str(e)}"
385
 
386
 
387
  # ============================================
388
- # CSS Styling - Comic Classic Theme
389
  # ============================================
390
- CSS = """
391
- /* Google Fonts */
 
392
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
393
 
394
- /* Background */
395
  .gradio-container {
396
  background-color: #FEF9C3 !important;
397
- background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
 
398
  background-size: 20px 20px !important;
399
  min-height: 100vh !important;
400
  font-family: 'Comic Neue', cursive, sans-serif !important;
401
  }
402
 
403
- /* Hide HuggingFace header */
404
- .huggingface-space-header, #space-header, .space-header,
405
- [class*="space-header"], .svelte-1ed2p3z, .space-header-badge {
 
 
 
 
 
 
 
 
 
406
  display: none !important;
 
 
 
 
 
 
407
  }
408
 
409
- /* Hide footer */
410
- footer, .footer, .gradio-container footer, .built-with,
411
- [class*="footer"], .show-api, .built-with-gradio {
 
 
 
 
 
 
 
 
 
 
412
  display: none !important;
 
 
 
 
413
  }
414
 
415
- /* Main container */
416
  #col-container {
417
  max-width: 1400px;
418
  margin: 0 auto;
419
  }
420
 
421
- /* Header */
422
  .header-text h1 {
423
  font-family: 'Bangers', cursive !important;
424
  color: #1F2937 !important;
425
  font-size: 3.5rem !important;
 
426
  text-align: center !important;
427
- text-shadow: 4px 4px 0px #FACC15, 6px 6px 0px #1F2937 !important;
 
 
 
428
  letter-spacing: 3px !important;
429
  -webkit-text-stroke: 2px #1F2937 !important;
430
  }
431
 
 
432
  .subtitle {
433
  text-align: center !important;
434
  font-family: 'Comic Neue', cursive !important;
435
  font-size: 1.2rem !important;
436
  color: #1F2937 !important;
 
437
  font-weight: 700 !important;
438
  }
439
 
440
- /* Stats cards */
441
  .stats-row {
442
  display: flex !important;
443
  justify-content: center !important;
@@ -456,11 +372,25 @@ footer, .footer, .gradio-container footer, .built-with,
456
  min-width: 120px !important;
457
  }
458
 
459
- .stat-card .emoji { font-size: 2rem !important; display: block !important; }
460
- .stat-card .label { color: #FFFFFF !important; font-family: 'Comic Neue', cursive !important; font-weight: 700 !important; }
 
 
 
461
 
462
- /* Panels */
463
- .gr-panel, .gr-box, .gr-form, .block, .gr-group {
 
 
 
 
 
 
 
 
 
 
 
464
  background: #FFFFFF !important;
465
  border: 3px solid #1F2937 !important;
466
  border-radius: 8px !important;
@@ -468,15 +398,17 @@ footer, .footer, .gradio-container footer, .built-with,
468
  transition: all 0.2s ease !important;
469
  }
470
 
471
- .gr-panel:hover, .block:hover {
 
472
  transform: translate(-2px, -2px) !important;
473
  box-shadow: 8px 8px 0px #1F2937 !important;
474
  }
475
 
476
- /* Tabs */
477
  .gr-tabs {
478
  border: 3px solid #1F2937 !important;
479
  border-radius: 12px !important;
 
480
  box-shadow: 6px 6px 0px #1F2937 !important;
481
  }
482
 
@@ -488,78 +420,163 @@ footer, .footer, .gradio-container footer, .built-with,
488
  .gr-tab-nav button {
489
  font-family: 'Bangers', cursive !important;
490
  font-size: 1.2rem !important;
 
491
  color: #1F2937 !important;
492
  padding: 12px 24px !important;
 
 
 
 
 
 
 
493
  }
494
 
495
  .gr-tab-nav button.selected {
496
  background: #3B82F6 !important;
497
  color: #FFFFFF !important;
 
498
  }
499
 
500
- /* Inputs */
501
- textarea, input[type="text"], input[type="number"] {
 
 
502
  background: #FFFFFF !important;
503
  border: 3px solid #1F2937 !important;
504
  border-radius: 8px !important;
 
505
  font-family: 'Comic Neue', cursive !important;
 
506
  font-weight: 700 !important;
 
507
  }
508
 
509
- textarea:focus, input:focus {
 
 
510
  border-color: #3B82F6 !important;
511
  box-shadow: 4px 4px 0px #3B82F6 !important;
 
512
  }
513
 
514
- /* Dropdown */
515
- .gr-dropdown {
516
- background: #FFFFFF !important;
517
  border: 3px solid #1F2937 !important;
518
  border-radius: 8px !important;
519
  box-shadow: 3px 3px 0px #1F2937 !important;
520
  }
521
 
522
- /* Primary button */
523
- .gr-button-primary, button.primary, .process-btn {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  background: #3B82F6 !important;
525
  border: 3px solid #1F2937 !important;
526
  border-radius: 8px !important;
527
  color: #FFFFFF !important;
528
  font-family: 'Bangers', cursive !important;
 
529
  font-size: 1.3rem !important;
530
  letter-spacing: 2px !important;
531
  padding: 14px 28px !important;
532
  box-shadow: 5px 5px 0px #1F2937 !important;
 
 
533
  }
534
 
535
- .gr-button-primary:hover, button.primary:hover {
 
 
 
536
  background: #2563EB !important;
537
  transform: translate(-2px, -2px) !important;
538
  box-shadow: 7px 7px 0px #1F2937 !important;
539
  }
540
 
541
- .gr-button-primary:active, button.primary:active {
 
 
 
542
  transform: translate(3px, 3px) !important;
543
  box-shadow: 2px 2px 0px #1F2937 !important;
544
  }
545
 
546
- /* Log output */
547
  .info-log textarea {
548
  background: #1F2937 !important;
549
  color: #10B981 !important;
550
  font-family: 'Courier New', monospace !important;
 
 
551
  border: 3px solid #10B981 !important;
 
552
  box-shadow: 4px 4px 0px #10B981 !important;
553
  }
554
 
555
- /* Image/Video containers */
556
- .gr-image, .gr-video {
 
 
 
557
  border: 4px solid #1F2937 !important;
558
  border-radius: 8px !important;
559
  box-shadow: 8px 8px 0px #1F2937 !important;
 
 
 
 
 
 
 
 
 
 
 
560
  }
561
 
562
- /* Accordion */
563
  .gr-accordion {
564
  background: #FACC15 !important;
565
  border: 3px solid #1F2937 !important;
@@ -567,114 +584,338 @@ textarea:focus, input:focus {
567
  box-shadow: 4px 4px 0px #1F2937 !important;
568
  }
569
 
570
- /* Labels */
571
- label, .gr-input-label, .gr-block-label {
572
  color: #1F2937 !important;
573
  font-family: 'Comic Neue', cursive !important;
574
  font-weight: 700 !important;
 
575
  }
576
 
577
- /* Slider */
578
- input[type="range"] { accent-color: #3B82F6 !important; }
 
 
 
 
 
 
 
579
 
580
- /* Scrollbar */
581
- ::-webkit-scrollbar { width: 12px; }
582
- ::-webkit-scrollbar-track { background: #FEF9C3; border: 2px solid #1F2937; }
583
- ::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; }
 
 
 
584
 
585
- /* Selection */
586
- ::selection { background: #FACC15; color: #1F2937; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
 
588
- /* Responsive */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  @media (max-width: 768px) {
590
- .header-text h1 { font-size: 2.2rem !important; }
591
- .gr-button-primary { padding: 12px 20px !important; font-size: 1.1rem !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  }
593
  """
594
 
595
 
596
  # ============================================
597
- # UI Component Builders
598
  # ============================================
599
- def create_header() -> None:
600
- """Create header section"""
 
601
  gr.HTML("""
602
  <div style="text-align: center; margin: 20px 0 10px 0;">
603
- <a href="https://www.humangen.ai" target="_blank">
604
  <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
605
  </a>
606
- <a href="https://discord.gg/openfreeai" target="_blank" style="margin-left: 10px;">
607
  <img src="https://img.shields.io/static/v1?label=Discord&message=OpenFree%20AI&color=5865F2&labelColor=1F2937&logo=discord&logoColor=white&style=for-the-badge" alt="Discord">
608
  </a>
609
  </div>
610
  """)
611
 
612
- gr.Markdown("# 🔒 ANSIM BLUR - FACE PRIVACY 🛡️", elem_classes="header-text")
613
- gr.Markdown('<p class="subtitle">🎭 Advanced AI-Powered Face Detection & Privacy Protection! ✨</p>')
 
 
 
 
 
 
 
 
 
 
 
614
 
 
615
  gr.HTML("""
616
  <div class="stats-row">
617
- <div class="stat-card"><span class="emoji">🖼️</span><span class="label">Image Support</span></div>
618
- <div class="stat-card"><span class="emoji">🎥</span><span class="label">Video Processing</span></div>
619
- <div class="stat-card"><span class="emoji">⚡</span><span class="label">Real-time AI</span></div>
620
- <div class="stat-card"><span class="emoji">🛡️</span><span class="label">Privacy First</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
621
  </div>
622
  """)
623
 
 
624
  gr.Markdown(f"""
625
- <p style="text-align: center; font-family: 'Comic Neue', cursive; font-weight: 700; color: #1F2937;">
626
- 🖥️ Running on: <span style="color: #3B82F6;">{detector.device.upper()}</span>
627
  </p>
628
  """)
629
-
630
-
631
- def create_detection_settings(suffix: str = "") -> Tuple[gr.Slider, gr.Slider, gr.Slider]:
632
- """Create detection settings accordion"""
633
- with gr.Accordion("⚙️ Detection Settings", open=True):
634
- conf = gr.Slider(
635
- **SLIDER_CONFIG["confidence"],
636
- value=DEFAULT_CONFIG["confidence"],
637
- label="🎯 Confidence Threshold"
638
- )
639
- iou = gr.Slider(
640
- **SLIDER_CONFIG["iou"],
641
- value=DEFAULT_CONFIG["iou"],
642
- label="📐 NMS IoU"
643
- )
644
- expand = gr.Slider(
645
- **SLIDER_CONFIG["expand"],
646
- value=DEFAULT_CONFIG["expand_ratio"],
647
- label="🔲 Box Expansion"
648
- )
649
- return conf, iou, expand
650
-
651
-
652
- def create_blur_settings(suffix: str = "") -> Tuple[gr.Dropdown, gr.Slider, gr.Slider]:
653
- """Create blur settings accordion"""
654
- with gr.Accordion("🎨 Blur Settings", open=True):
655
- mode = gr.Dropdown(
656
- choices=BLUR_MODES,
657
- value=BLUR_MODES[0],
658
- label="🖌️ Style"
659
- )
660
- blur_intensity = gr.Slider(
661
- **SLIDER_CONFIG["blur"],
662
- value=DEFAULT_CONFIG["blur_intensity"],
663
- label="💨 Blur Intensity"
664
- )
665
- mosaic_size = gr.Slider(
666
- **SLIDER_CONFIG["mosaic"],
667
- value=DEFAULT_CONFIG["mosaic_size"],
668
- label="🧩 Mosaic Size"
669
- )
670
- return mode, blur_intensity, mosaic_size
671
-
672
-
673
- def create_footer() -> None:
674
- """Create footer with instructions"""
675
- gr.Markdown("""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  <div style="background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%); border: 3px solid #3B82F6; border-radius: 12px; padding: 1.5rem; box-shadow: 6px 6px 0px #1F2937; margin-top: 2rem;">
677
- <h3 style="font-family: 'Bangers', cursive; color: #1F2937; font-size: 1.3rem;">📝 HOW TO USE</h3>
678
  <ol style="font-family: 'Comic Neue', cursive; color: #1F2937; font-weight: 700;">
679
  <li>Upload an image or video containing faces</li>
680
  <li>Adjust detection settings (confidence, IoU, expansion)</li>
@@ -685,7 +926,7 @@ def create_footer() -> None:
685
  </div>
686
 
687
  <div style="background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%); border: 3px solid #F59E0B; border-radius: 12px; padding: 1.5rem; box-shadow: 6px 6px 0px #1F2937; margin-top: 1rem;">
688
- <h3 style="font-family: 'Bangers', cursive; color: #1F2937; font-size: 1.3rem;">💡 TIPS</h3>
689
  <ul style="font-family: 'Comic Neue', cursive; color: #1F2937; font-weight: 700;">
690
  <li>Lower confidence = more faces detected (may include false positives)</li>
691
  <li>Higher blur intensity = stronger privacy protection</li>
@@ -693,128 +934,38 @@ def create_footer() -> None:
693
  <li>Video processing may take time depending on length</li>
694
  </ul>
695
  </div>
696
- """)
697
-
698
-
699
- # ============================================
700
- # Main Application Builder
701
- # ============================================
702
- def create_app() -> gr.Blocks:
703
- """Build and return the Gradio application"""
704
 
705
- with gr.Blocks(
706
- fill_height=True,
707
- css=CSS,
708
- title="Ansim Blur - Face Privacy Protection",
709
- theme=gr.themes.Default()
710
- ) as app:
711
-
712
- # Header
713
- create_header()
714
-
715
- # Main tabs
716
- with gr.Tabs():
717
-
718
- # === IMAGE TAB ===
719
- with gr.Tab("📸 Image Processing"):
720
- with gr.Row(equal_height=False):
721
-
722
- # Left column - Input
723
- with gr.Column(scale=1, min_width=400):
724
- input_image = gr.Image(
725
- label="🖼️ Upload Image",
726
- type="pil",
727
- height=350
728
- )
729
-
730
- conf_img, iou_img, expand_img = create_detection_settings("img")
731
- mode_img, blur_img, mosaic_img = create_blur_settings("img")
732
-
733
- process_img_btn = gr.Button(
734
- "🔍 PROCESS IMAGE! 🎭",
735
- variant="primary",
736
- size="lg",
737
- elem_classes="process-btn"
738
- )
739
-
740
- # Right column - Output
741
- with gr.Column(scale=1, min_width=400):
742
- output_image = gr.Image(
743
- label="🖼️ Processed Result",
744
- type="pil",
745
- height=350
746
- )
747
-
748
- with gr.Accordion("📜 Processing Log", open=True):
749
- info_log_img = gr.Textbox(
750
- label="",
751
- placeholder="Upload an image and click process...",
752
- lines=12,
753
- max_lines=18,
754
- interactive=False,
755
- elem_classes="info-log"
756
- )
757
-
758
- # === VIDEO TAB ===
759
- with gr.Tab("🎬 Video Processing"):
760
- with gr.Row(equal_height=False):
761
-
762
- # Left column - Input
763
- with gr.Column(scale=1, min_width=400):
764
- input_video = gr.Video(
765
- label="🎥 Upload Video",
766
- height=350
767
- )
768
-
769
- conf_vid, iou_vid, expand_vid = create_detection_settings("vid")
770
- mode_vid, blur_vid, mosaic_vid = create_blur_settings("vid")
771
-
772
- process_vid_btn = gr.Button(
773
- "🎬 PROCESS VIDEO! 🛡️",
774
- variant="primary",
775
- size="lg",
776
- elem_classes="process-btn"
777
- )
778
-
779
- # Right column - Output
780
- with gr.Column(scale=1, min_width=400):
781
- output_video = gr.Video(
782
- label="🎥 Processed Result",
783
- height=350
784
- )
785
-
786
- with gr.Accordion("📜 Processing Log", open=True):
787
- info_log_vid = gr.Textbox(
788
- label="",
789
- placeholder="Upload a video and click process...",
790
- lines=12,
791
- max_lines=18,
792
- interactive=False,
793
- elem_classes="info-log"
794
- )
795
-
796
- # Footer
797
- create_footer()
798
-
799
- # === EVENT HANDLERS ===
800
- process_img_btn.click(
801
- fn=process_image,
802
- inputs=[input_image, conf_img, iou_img, expand_img, mode_img, blur_img, mosaic_img],
803
- outputs=[output_image, info_log_img]
804
- )
805
-
806
- process_vid_btn.click(
807
- fn=process_video,
808
- inputs=[input_video, conf_vid, iou_vid, expand_vid, mode_vid, blur_vid, mosaic_vid],
809
- outputs=[output_video, info_log_vid]
810
- )
811
 
812
- return app
 
 
 
 
 
 
 
 
 
 
 
 
813
 
814
 
815
- # ============================================
816
- # Entry Point
817
- # ============================================
818
  if __name__ == "__main__":
819
- app = create_app()
820
- app.launch()
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import cv2
3
  import numpy as np
4
  import tempfile
5
  import os
6
  from pathlib import Path
7
+ from typing import Optional, Tuple
8
  import torch
9
  from PIL import Image
10
 
11
+ # ==============================
12
+ # Model loader
13
+ # ==============================
14
+ def load_model(model_path: str = "yolov8-face-hf.pt", device: Optional[str] = None):
15
+ from ultralytics import YOLO
16
+ if device is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  if torch.cuda.is_available():
18
+ device = "cuda"
19
  elif torch.backends.mps.is_available():
20
+ device = "mps"
21
+ else:
22
+ device = "cpu"
23
+ model = YOLO(model_path)
24
+ model.to(device)
25
+ return model, device
26
+
27
+ # Load model globally
28
+ model, device = load_model()
29
+
30
+ # ==============================
31
+ # Helper functions
32
+ # ==============================
33
+ def _ensure_odd(x: int) -> int:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  return x if x % 2 == 1 else x + 1
35
 
36
+ def _choose_writer_size(w: int, h: int) -> Tuple[int, int]:
 
 
37
  return (w if w % 2 == 0 else w - 1, h if h % 2 == 0 else h - 1)
38
 
39
+ def _apply_anonymization(face_roi: np.ndarray, mode: str, blur_kernel: int, mosaic: int = 15) -> np.ndarray:
 
 
 
 
 
 
 
40
  if face_roi.size == 0:
41
  return face_roi
 
42
  if mode == "Gaussian Blur":
43
+ k = _ensure_odd(max(blur_kernel, 15))
44
  return cv2.GaussianBlur(face_roi, (k, k), 0)
45
+ else:
46
+ m = max(2, mosaic)
47
  h, w = face_roi.shape[:2]
48
+ face_small = cv2.resize(face_roi, (max(1, w // m), max(1, h // m)), interpolation=cv2.INTER_LINEAR)
49
+ return cv2.resize(face_small, (w, h), interpolation=cv2.INTER_NEAREST)
50
+
51
+ def blur_faces_image(image_bgr, conf, iou, expand_ratio, mode, blur_kernel, mosaic):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  h, w = image_bgr.shape[:2]
53
  face_count = 0
54
+
55
+ with torch.no_grad():
56
+ results = model.predict(image_bgr, conf=conf, iou=iou, verbose=False, device=device)
57
+
58
  for r in results:
59
  boxes = r.boxes.xyxy.cpu().numpy() if hasattr(r.boxes, "xyxy") else []
60
  face_count = len(boxes)
61
+ for x1, y1, x2, y2 in boxes:
62
+ x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])
63
+
64
+ if expand_ratio > 0:
65
+ bw = x2 - x1
66
+ bh = y2 - y1
67
+ dx = int(bw * expand_ratio)
68
+ dy = int(bh * expand_ratio)
69
+ x1 -= dx; y1 -= dy; x2 += dx; y2 += dy
70
+
71
+ x1 = max(0, min(w, x1))
72
+ x2 = max(0, min(w, x2))
73
+ y1 = max(0, min(h, y1))
74
+ y2 = max(0, min(h, y2))
75
  if x2 <= x1 or y2 <= y1:
76
  continue
77
+
78
  roi = image_bgr[y1:y2, x1:x2]
79
+ image_bgr[y1:y2, x1:x2] = _apply_anonymization(roi, mode, blur_kernel, mosaic)
 
 
80
 
81
+ return image_bgr, face_count
82
 
83
+ def blur_faces_video(input_path, conf, iou, expand_ratio, mode, blur_kernel, mosaic, progress=gr.Progress()):
 
 
 
 
 
 
 
 
 
 
84
  from moviepy.editor import VideoFileClip
85
 
86
  cap = cv2.VideoCapture(input_path)
87
  if not cap.isOpened():
88
+ raise IOError("Cannot open video")
89
+
 
90
  in_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
91
  in_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
92
  fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
93
+ frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
94
+
95
+ out_w, out_h = _choose_writer_size(in_w, in_h)
 
 
 
 
 
 
 
 
 
 
96
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
 
97
 
98
+ temp_video_path = tempfile.NamedTemporaryFile(delete=False, suffix="_temp.mp4").name
99
+ output_path = tempfile.NamedTemporaryFile(delete=False, suffix="_blurred.mp4").name
100
+
101
+ out = cv2.VideoWriter(temp_video_path, fourcc, fps, (out_w, out_h))
102
+
103
+ idx = 0
104
  total_faces = 0
105
 
106
  try:
 
108
  ret, frame = cap.read()
109
  if not ret:
110
  break
 
111
  frame = cv2.resize(frame, (out_w, out_h))
112
+
113
+ with torch.no_grad():
114
+ results = model.predict(frame, conf=conf, iou=iou, verbose=False, device=device)
115
+
116
  h, w = frame.shape[:2]
117
+ r0 = results[0] if len(results) else None
118
+ boxes = r0.boxes.xyxy if (r0 and hasattr(r0, "boxes")) else []
119
+ total_faces += len(boxes)
120
+
121
+ for b in boxes:
122
+ x1, y1, x2, y2 = map(int, b)
123
+ if expand_ratio > 0:
124
+ bw = x2 - x1
125
+ bh = y2 - y1
126
+ dx = int(bw * expand_ratio)
127
+ dy = int(bh * expand_ratio)
128
+ x1 -= dx; y1 -= dy; x2 += dx; y2 += dy
129
+
130
+ x1 = max(0, min(w, x1))
131
+ x2 = max(0, min(w, x2))
132
+ y1 = max(0, min(h, y1))
133
+ y2 = max(0, min(h, y2))
134
+ if x2 <= x1 or y2 <= y1:
135
+ continue
136
+
137
+ roi = frame[y1:y2, x1:x2]
138
+ frame[y1:y2, x1:x2] = _apply_anonymization(roi, mode, blur_kernel, mosaic)
139
+
140
+ out.write(frame)
141
+ idx += 1
142
+ if frames > 0:
143
+ progress(idx / frames, desc=f"Processing frame {idx}/{frames}")
144
  finally:
145
  cap.release()
146
+ out.release()
147
+
 
148
  try:
149
  progress(0.95, desc="Merging audio...")
150
  original = VideoFileClip(input_path)
151
+ processed = VideoFileClip(temp_video_path).set_audio(original.audio)
152
  processed.write_videofile(
153
  output_path,
154
  codec="libx264",
 
158
  )
159
  original.close()
160
  processed.close()
161
+ return output_path, total_faces, frames
 
 
 
 
 
 
162
  except Exception as e:
163
+ print("Audio merging failed:", e)
164
+ return temp_video_path, total_faces, frames
165
 
166
 
167
+ # ==============================
168
+ # Main Processing Functions
169
+ # ==============================
170
+ def process_image(image, conf, iou, expand_ratio, mode_choice, blur_intensity, mosaic_size):
 
 
 
 
 
 
 
 
 
171
  if image is None:
172
  return None, "⚠️ Please upload an image first!"
173
 
 
175
  image_bgr = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
176
  h, w = image_bgr.shape[:2]
177
 
178
+ # Determine blur settings
179
+ if mode_choice == "Gaussian Blur":
180
+ blur_kernel = blur_intensity
181
+ mosaic = 15
182
+ else:
183
+ blur_kernel = 51
184
+ mosaic = mosaic_size
185
 
186
+ # Process
187
+ result_bgr, face_count = blur_faces_image(
188
  image_bgr.copy(), conf, iou, expand_ratio,
189
  mode_choice, blur_kernel, mosaic
190
  )
 
194
  result_pil = Image.fromarray(result_rgb)
195
 
196
  # Generate log
 
197
  info_log = f"""✅ IMAGE PROCESSING COMPLETE!
198
  {'=' * 50}
199
  🖼️ Image Info:
 
207
  {'=' * 50}
208
  🎨 Blur Settings:
209
  • Style: {mode_choice}
210
+ • Intensity: {blur_intensity if mode_choice == "Gaussian Blur" else mosaic_size}
211
  {'=' * 50}
212
  👤 Results:
213
  • Faces Detected: {face_count}
 
218
  return result_pil, info_log
219
 
220
 
221
+ def process_video(video, conf, iou, expand_ratio, mode_choice, blur_intensity, mosaic_size, progress=gr.Progress()):
 
 
 
 
 
 
 
 
 
 
222
  if video is None:
223
  return None, "⚠️ Please upload a video first!"
224
 
225
+ # Determine blur settings
226
+ if mode_choice == "Gaussian Blur":
227
+ blur_kernel = blur_intensity
228
+ mosaic = 15
229
+ else:
230
+ blur_kernel = 51
231
+ mosaic = mosaic_size
232
 
233
  try:
234
+ output_path, total_faces, total_frames = blur_faces_video(
235
  video, conf, iou, expand_ratio,
236
  mode_choice, blur_kernel, mosaic, progress
237
  )
238
 
 
239
  info_log = f"""✅ VIDEO PROCESSING COMPLETE!
240
  {'=' * 50}
241
  🎥 Video Info:
 
249
  {'=' * 50}
250
  🎨 Blur Settings:
251
  • Style: {mode_choice}
252
+ • Intensity: {blur_intensity if mode_choice == "Gaussian Blur" else mosaic_size}
253
  {'=' * 50}
254
  👤 Results:
255
  • Total Faces Detected: {total_faces}
 
258
  💾 Ready to download!"""
259
 
260
  return output_path, info_log
261
+
262
  except Exception as e:
263
  return None, f"❌ Error: {str(e)}"
264
 
265
 
266
  # ============================================
267
+ # 🎨 Comic Classic Theme - Toon Playground
268
  # ============================================
269
+
270
+ css = """
271
+ /* ===== 🎨 Google Fonts Import ===== */
272
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
273
 
274
+ /* ===== 🎨 Comic Classic 배경 - 빈티지 페이퍼 + 도트 패턴 ===== */
275
  .gradio-container {
276
  background-color: #FEF9C3 !important;
277
+ background-image:
278
+ radial-gradient(#1F2937 1px, transparent 1px) !important;
279
  background-size: 20px 20px !important;
280
  min-height: 100vh !important;
281
  font-family: 'Comic Neue', cursive, sans-serif !important;
282
  }
283
 
284
+ /* ===== 허깅페이스 상단 요소 숨김 ===== */
285
+ .huggingface-space-header,
286
+ #space-header,
287
+ .space-header,
288
+ [class*="space-header"],
289
+ .svelte-1ed2p3z,
290
+ .space-header-badge,
291
+ .header-badge,
292
+ [data-testid="space-header"],
293
+ .svelte-kqij2n,
294
+ .svelte-1ax1toq,
295
+ .embed-container > div:first-child {
296
  display: none !important;
297
+ visibility: hidden !important;
298
+ height: 0 !important;
299
+ width: 0 !important;
300
+ overflow: hidden !important;
301
+ opacity: 0 !important;
302
+ pointer-events: none !important;
303
  }
304
 
305
+ /* ===== Footer 완전 숨김 ===== */
306
+ footer,
307
+ .footer,
308
+ .gradio-container footer,
309
+ .built-with,
310
+ [class*="footer"],
311
+ .gradio-footer,
312
+ .main-footer,
313
+ div[class*="footer"],
314
+ .show-api,
315
+ .built-with-gradio,
316
+ a[href*="gradio.app"],
317
+ a[href*="huggingface.co/spaces"] {
318
  display: none !important;
319
+ visibility: hidden !important;
320
+ height: 0 !important;
321
+ padding: 0 !important;
322
+ margin: 0 !important;
323
  }
324
 
325
+ /* ===== 메인 컨테이너 ===== */
326
  #col-container {
327
  max-width: 1400px;
328
  margin: 0 auto;
329
  }
330
 
331
+ /* ===== 🎨 헤더 타이틀 - 코믹 스타일 ===== */
332
  .header-text h1 {
333
  font-family: 'Bangers', cursive !important;
334
  color: #1F2937 !important;
335
  font-size: 3.5rem !important;
336
+ font-weight: 400 !important;
337
  text-align: center !important;
338
+ margin-bottom: 0.5rem !important;
339
+ text-shadow:
340
+ 4px 4px 0px #FACC15,
341
+ 6px 6px 0px #1F2937 !important;
342
  letter-spacing: 3px !important;
343
  -webkit-text-stroke: 2px #1F2937 !important;
344
  }
345
 
346
+ /* ===== 🎨 서브타이틀 ===== */
347
  .subtitle {
348
  text-align: center !important;
349
  font-family: 'Comic Neue', cursive !important;
350
  font-size: 1.2rem !important;
351
  color: #1F2937 !important;
352
+ margin-bottom: 1.5rem !important;
353
  font-weight: 700 !important;
354
  }
355
 
356
+ /* ===== 🎨 Stats 카드 ===== */
357
  .stats-row {
358
  display: flex !important;
359
  justify-content: center !important;
 
372
  min-width: 120px !important;
373
  }
374
 
375
+ .stat-card .emoji {
376
+ font-size: 2rem !important;
377
+ display: block !important;
378
+ margin-bottom: 0.3rem !important;
379
+ }
380
 
381
+ .stat-card .label {
382
+ color: #FFFFFF !important;
383
+ font-family: 'Comic Neue', cursive !important;
384
+ font-weight: 700 !important;
385
+ font-size: 0.9rem !important;
386
+ }
387
+
388
+ /* ===== 🎨 카드/패널 - 만화 프레임 스타일 ===== */
389
+ .gr-panel,
390
+ .gr-box,
391
+ .gr-form,
392
+ .block,
393
+ .gr-group {
394
  background: #FFFFFF !important;
395
  border: 3px solid #1F2937 !important;
396
  border-radius: 8px !important;
 
398
  transition: all 0.2s ease !important;
399
  }
400
 
401
+ .gr-panel:hover,
402
+ .block:hover {
403
  transform: translate(-2px, -2px) !important;
404
  box-shadow: 8px 8px 0px #1F2937 !important;
405
  }
406
 
407
+ /* ===== 🎨 탭 스타일 ===== */
408
  .gr-tabs {
409
  border: 3px solid #1F2937 !important;
410
  border-radius: 12px !important;
411
+ overflow: hidden !important;
412
  box-shadow: 6px 6px 0px #1F2937 !important;
413
  }
414
 
 
420
  .gr-tab-nav button {
421
  font-family: 'Bangers', cursive !important;
422
  font-size: 1.2rem !important;
423
+ letter-spacing: 1px !important;
424
  color: #1F2937 !important;
425
  padding: 12px 24px !important;
426
+ border: none !important;
427
+ background: transparent !important;
428
+ transition: all 0.2s ease !important;
429
+ }
430
+
431
+ .gr-tab-nav button:hover {
432
+ background: #FDE68A !important;
433
  }
434
 
435
  .gr-tab-nav button.selected {
436
  background: #3B82F6 !important;
437
  color: #FFFFFF !important;
438
+ text-shadow: 1px 1px 0px #1F2937 !important;
439
  }
440
 
441
+ /* ===== 🎨 입력 필드 ===== */
442
+ textarea,
443
+ input[type="text"],
444
+ input[type="number"] {
445
  background: #FFFFFF !important;
446
  border: 3px solid #1F2937 !important;
447
  border-radius: 8px !important;
448
+ color: #1F2937 !important;
449
  font-family: 'Comic Neue', cursive !important;
450
+ font-size: 1rem !important;
451
  font-weight: 700 !important;
452
+ transition: all 0.2s ease !important;
453
  }
454
 
455
+ textarea:focus,
456
+ input[type="text"]:focus,
457
+ input[type="number"]:focus {
458
  border-color: #3B82F6 !important;
459
  box-shadow: 4px 4px 0px #3B82F6 !important;
460
+ outline: none !important;
461
  }
462
 
463
+ /* ===== 🎨 드롭다운 스타일 ===== */
464
+ [data-testid="dropdown"] {
 
465
  border: 3px solid #1F2937 !important;
466
  border-radius: 8px !important;
467
  box-shadow: 3px 3px 0px #1F2937 !important;
468
  }
469
 
470
+ [data-testid="dropdown"] .wrap {
471
+ background: #FFFFFF !important;
472
+ }
473
+
474
+ [data-testid="dropdown"] .wrap-inner {
475
+ background: #FFFFFF !important;
476
+ }
477
+
478
+ [data-testid="dropdown"] .secondary-wrap {
479
+ background: #FFFFFF !important;
480
+ }
481
+
482
+ [data-testid="dropdown"] input {
483
+ color: #1F2937 !important;
484
+ font-family: 'Comic Neue', cursive !important;
485
+ font-weight: 700 !important;
486
+ }
487
+
488
+ [data-testid="dropdown"] .options {
489
+ background: #FFFFFF !important;
490
+ border: 3px solid #1F2937 !important;
491
+ border-radius: 8px !important;
492
+ box-shadow: 4px 4px 0px #1F2937 !important;
493
+ }
494
+
495
+ [data-testid="dropdown"] .item {
496
+ color: #1F2937 !important;
497
+ font-family: 'Comic Neue', cursive !important;
498
+ font-weight: 700 !important;
499
+ }
500
+
501
+ [data-testid="dropdown"] .item:hover {
502
+ background: #FACC15 !important;
503
+ }
504
+
505
+ [data-testid="dropdown"] .item.selected {
506
+ background: #3B82F6 !important;
507
+ color: #FFFFFF !important;
508
+ }
509
+
510
+ /* ===== 🎨 Primary 버튼 ===== */
511
+ .gr-button-primary,
512
+ button.primary,
513
+ .gr-button.primary,
514
+ .process-btn {
515
  background: #3B82F6 !important;
516
  border: 3px solid #1F2937 !important;
517
  border-radius: 8px !important;
518
  color: #FFFFFF !important;
519
  font-family: 'Bangers', cursive !important;
520
+ font-weight: 400 !important;
521
  font-size: 1.3rem !important;
522
  letter-spacing: 2px !important;
523
  padding: 14px 28px !important;
524
  box-shadow: 5px 5px 0px #1F2937 !important;
525
+ transition: all 0.1s ease !important;
526
+ text-shadow: 1px 1px 0px #1F2937 !important;
527
  }
528
 
529
+ .gr-button-primary:hover,
530
+ button.primary:hover,
531
+ .gr-button.primary:hover,
532
+ .process-btn:hover {
533
  background: #2563EB !important;
534
  transform: translate(-2px, -2px) !important;
535
  box-shadow: 7px 7px 0px #1F2937 !important;
536
  }
537
 
538
+ .gr-button-primary:active,
539
+ button.primary:active,
540
+ .gr-button.primary:active,
541
+ .process-btn:active {
542
  transform: translate(3px, 3px) !important;
543
  box-shadow: 2px 2px 0px #1F2937 !important;
544
  }
545
 
546
+ /* ===== 🎨 로그 출력 영역 ===== */
547
  .info-log textarea {
548
  background: #1F2937 !important;
549
  color: #10B981 !important;
550
  font-family: 'Courier New', monospace !important;
551
+ font-size: 0.9rem !important;
552
+ font-weight: 400 !important;
553
  border: 3px solid #10B981 !important;
554
+ border-radius: 8px !important;
555
  box-shadow: 4px 4px 0px #10B981 !important;
556
  }
557
 
558
+ /* ===== 🎨 이미지/비디오 영역 ===== */
559
+ .gr-image,
560
+ .gr-video,
561
+ .image-container,
562
+ .video-container {
563
  border: 4px solid #1F2937 !important;
564
  border-radius: 8px !important;
565
  box-shadow: 8px 8px 0px #1F2937 !important;
566
+ overflow: hidden !important;
567
+ background: #FFFFFF !important;
568
+ }
569
+
570
+ /* ===== 🎨 슬라이더 스타일 ===== */
571
+ input[type="range"] {
572
+ accent-color: #3B82F6 !important;
573
+ }
574
+
575
+ .gr-slider {
576
+ background: #FFFFFF !important;
577
  }
578
 
579
+ /* ===== 🎨 아코디언 ===== */
580
  .gr-accordion {
581
  background: #FACC15 !important;
582
  border: 3px solid #1F2937 !important;
 
584
  box-shadow: 4px 4px 0px #1F2937 !important;
585
  }
586
 
587
+ .gr-accordion-header {
 
588
  color: #1F2937 !important;
589
  font-family: 'Comic Neue', cursive !important;
590
  font-weight: 700 !important;
591
+ font-size: 1.1rem !important;
592
  }
593
 
594
+ /* ===== 🎨 라벨 스타일 ===== */
595
+ label,
596
+ .gr-input-label,
597
+ .gr-block-label {
598
+ color: #1F2937 !important;
599
+ font-family: 'Comic Neue', cursive !important;
600
+ font-weight: 700 !important;
601
+ font-size: 1rem !important;
602
+ }
603
 
604
+ /* ===== 🎨 프로그레스 바 ===== */
605
+ .progress-bar,
606
+ .gr-progress-bar {
607
+ background: #3B82F6 !important;
608
+ border: 2px solid #1F2937 !important;
609
+ border-radius: 4px !important;
610
+ }
611
 
612
+ /* ===== 🎨 스크롤바 ===== */
613
+ ::-webkit-scrollbar {
614
+ width: 12px;
615
+ height: 12px;
616
+ }
617
+
618
+ ::-webkit-scrollbar-track {
619
+ background: #FEF9C3;
620
+ border: 2px solid #1F2937;
621
+ }
622
+
623
+ ::-webkit-scrollbar-thumb {
624
+ background: #3B82F6;
625
+ border: 2px solid #1F2937;
626
+ border-radius: 0px;
627
+ }
628
+
629
+ ::-webkit-scrollbar-thumb:hover {
630
+ background: #EF4444;
631
+ }
632
 
633
+ /* ===== 🎨 선택 하이라이트 ===== */
634
+ ::selection {
635
+ background: #FACC15;
636
+ color: #1F2937;
637
+ }
638
+
639
+ /* ===== 🎨 링크 스타일 ===== */
640
+ a {
641
+ color: #3B82F6 !important;
642
+ text-decoration: none !important;
643
+ font-weight: 700 !important;
644
+ }
645
+
646
+ a:hover {
647
+ color: #EF4444 !important;
648
+ }
649
+
650
+ /* ===== 🎨 Row/Column 간격 ===== */
651
+ .gr-row {
652
+ gap: 1.5rem !important;
653
+ }
654
+
655
+ .gr-column {
656
+ gap: 1rem !important;
657
+ }
658
+
659
+ /* ===== 반응형 조정 ===== */
660
  @media (max-width: 768px) {
661
+ .header-text h1 {
662
+ font-size: 2.2rem !important;
663
+ text-shadow:
664
+ 3px 3px 0px #FACC15,
665
+ 4px 4px 0px #1F2937 !important;
666
+ }
667
+
668
+ .gr-button-primary,
669
+ button.primary {
670
+ padding: 12px 20px !important;
671
+ font-size: 1.1rem !important;
672
+ }
673
+
674
+ .gr-panel,
675
+ .block {
676
+ box-shadow: 4px 4px 0px #1F2937 !important;
677
+ }
678
+
679
+ .stat-card {
680
+ min-width: 100px !important;
681
+ padding: 0.8rem 1rem !important;
682
+ }
683
+ }
684
+
685
+ /* ===== 🎨 다크모드 비활성화 ===== */
686
+ @media (prefers-color-scheme: dark) {
687
+ .gradio-container {
688
+ background-color: #FEF9C3 !important;
689
+ }
690
  }
691
  """
692
 
693
 
694
  # ============================================
695
+ # Build the Gradio Interface
696
  # ============================================
697
+ with gr.Blocks(fill_height=True, title="Ansim Blur - Face Privacy Protection") as demo:
698
+
699
+ # HOME Badge
700
  gr.HTML("""
701
  <div style="text-align: center; margin: 20px 0 10px 0;">
702
+ <a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
703
  <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
704
  </a>
705
+ <a href="https://discord.gg/openfreeai" target="_blank" style="text-decoration: none; margin-left: 10px;">
706
  <img src="https://img.shields.io/static/v1?label=Discord&message=OpenFree%20AI&color=5865F2&labelColor=1F2937&logo=discord&logoColor=white&style=for-the-badge" alt="Discord">
707
  </a>
708
  </div>
709
  """)
710
 
711
+ # Header Title
712
+ gr.Markdown(
713
+ """
714
+ # 🔒 ANSIM BLUR - FACE PRIVACY 🛡️
715
+ """,
716
+ elem_classes="header-text"
717
+ )
718
+
719
+ gr.Markdown(
720
+ """
721
+ <p class="subtitle">🎭 Advanced AI-Powered Face Detection & Privacy Protection! ✨</p>
722
+ """,
723
+ )
724
 
725
+ # Stats Cards
726
  gr.HTML("""
727
  <div class="stats-row">
728
+ <div class="stat-card">
729
+ <span class="emoji">🖼️</span>
730
+ <span class="label">Image Support</span>
731
+ </div>
732
+ <div class="stat-card">
733
+ <span class="emoji">🎥</span>
734
+ <span class="label">Video Processing</span>
735
+ </div>
736
+ <div class="stat-card">
737
+ <span class="emoji">⚡</span>
738
+ <span class="label">Real-time AI</span>
739
+ </div>
740
+ <div class="stat-card">
741
+ <span class="emoji">🛡️</span>
742
+ <span class="label">Privacy First</span>
743
+ </div>
744
  </div>
745
  """)
746
 
747
+ # Device Info
748
  gr.Markdown(f"""
749
+ <p style="text-align: center; font-family: 'Comic Neue', cursive; font-weight: 700; color: #1F2937; margin: 1rem 0;">
750
+ 🖥️ Running on: <span style="color: #3B82F6;">{device.upper()}</span>
751
  </p>
752
  """)
753
+
754
+ # Main Tabs
755
+ with gr.Tabs():
756
+ # ===== IMAGE TAB =====
757
+ with gr.Tab("📸 Image Processing"):
758
+ with gr.Row(equal_height=False):
759
+ # Left Column - Input & Settings
760
+ with gr.Column(scale=1, min_width=400):
761
+ input_image = gr.Image(
762
+ label="🖼️ Upload Image",
763
+ type="pil",
764
+ height=350
765
+ )
766
+
767
+ with gr.Accordion("⚙️ Detection Settings", open=True):
768
+ conf_img = gr.Slider(
769
+ minimum=0.05,
770
+ maximum=0.9,
771
+ value=0.25,
772
+ step=0.01,
773
+ label="🎯 Confidence Threshold"
774
+ )
775
+ iou_img = gr.Slider(
776
+ minimum=0.1,
777
+ maximum=0.9,
778
+ value=0.45,
779
+ step=0.01,
780
+ label="📐 NMS IoU"
781
+ )
782
+ expand_img = gr.Slider(
783
+ minimum=0.0,
784
+ maximum=0.5,
785
+ value=0.05,
786
+ step=0.01,
787
+ label="🔲 Box Expansion"
788
+ )
789
+
790
+ with gr.Accordion("🎨 Blur Settings", open=True):
791
+ mode_img = gr.Dropdown(
792
+ choices=["Gaussian Blur", "Mosaic Effect"],
793
+ value="Gaussian Blur",
794
+ label="🖌️ Style"
795
+ )
796
+ blur_intensity_img = gr.Slider(
797
+ minimum=15,
798
+ maximum=151,
799
+ value=51,
800
+ step=2,
801
+ label="💨 Blur Intensity"
802
+ )
803
+ mosaic_size_img = gr.Slider(
804
+ minimum=5,
805
+ maximum=40,
806
+ value=15,
807
+ step=1,
808
+ label="🧩 Mosaic Size"
809
+ )
810
+
811
+ process_img_btn = gr.Button(
812
+ "🔍 PROCESS IMAGE! 🎭",
813
+ variant="primary",
814
+ size="lg",
815
+ elem_classes="process-btn"
816
+ )
817
+
818
+ # Right Column - Output
819
+ with gr.Column(scale=1, min_width=400):
820
+ output_image = gr.Image(
821
+ label="🖼️ Processed Result",
822
+ type="pil",
823
+ height=350
824
+ )
825
+
826
+ with gr.Accordion("📜 Processing Log", open=True):
827
+ info_log_img = gr.Textbox(
828
+ label="",
829
+ placeholder="Upload an image and click process...",
830
+ lines=12,
831
+ max_lines=18,
832
+ interactive=False,
833
+ elem_classes="info-log"
834
+ )
835
+
836
+ # ===== VIDEO TAB =====
837
+ with gr.Tab("🎬 Video Processing"):
838
+ with gr.Row(equal_height=False):
839
+ # Left Column - Input & Settings
840
+ with gr.Column(scale=1, min_width=400):
841
+ input_video = gr.Video(
842
+ label="🎥 Upload Video",
843
+ height=350
844
+ )
845
+
846
+ with gr.Accordion("⚙️ Detection Settings", open=True):
847
+ conf_vid = gr.Slider(
848
+ minimum=0.05,
849
+ maximum=0.9,
850
+ value=0.25,
851
+ step=0.01,
852
+ label="🎯 Confidence Threshold"
853
+ )
854
+ iou_vid = gr.Slider(
855
+ minimum=0.1,
856
+ maximum=0.9,
857
+ value=0.45,
858
+ step=0.01,
859
+ label="📐 NMS IoU"
860
+ )
861
+ expand_vid = gr.Slider(
862
+ minimum=0.0,
863
+ maximum=0.5,
864
+ value=0.05,
865
+ step=0.01,
866
+ label="🔲 Box Expansion"
867
+ )
868
+
869
+ with gr.Accordion("🎨 Blur Settings", open=True):
870
+ mode_vid = gr.Dropdown(
871
+ choices=["Gaussian Blur", "Mosaic Effect"],
872
+ value="Gaussian Blur",
873
+ label="🖌️ Style"
874
+ )
875
+ blur_intensity_vid = gr.Slider(
876
+ minimum=15,
877
+ maximum=151,
878
+ value=51,
879
+ step=2,
880
+ label="💨 Blur Intensity"
881
+ )
882
+ mosaic_size_vid = gr.Slider(
883
+ minimum=5,
884
+ maximum=40,
885
+ value=15,
886
+ step=1,
887
+ label="🧩 Mosaic Size"
888
+ )
889
+
890
+ process_vid_btn = gr.Button(
891
+ "🎬 PROCESS VIDEO! 🛡️",
892
+ variant="primary",
893
+ size="lg",
894
+ elem_classes="process-btn"
895
+ )
896
+
897
+ # Right Column - Output
898
+ with gr.Column(scale=1, min_width=400):
899
+ output_video = gr.Video(
900
+ label="🎥 Processed Result",
901
+ height=350
902
+ )
903
+
904
+ with gr.Accordion("📜 Processing Log", open=True):
905
+ info_log_vid = gr.Textbox(
906
+ label="",
907
+ placeholder="Upload a video and click process...",
908
+ lines=12,
909
+ max_lines=18,
910
+ interactive=False,
911
+ elem_classes="info-log"
912
+ )
913
+
914
+ # Instructions
915
+ gr.Markdown(
916
+ """
917
  <div style="background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%); border: 3px solid #3B82F6; border-radius: 12px; padding: 1.5rem; box-shadow: 6px 6px 0px #1F2937; margin-top: 2rem;">
918
+ <h3 style="font-family: 'Bangers', cursive; color: #1F2937; font-size: 1.3rem; margin-bottom: 0.5rem;">📝 HOW TO USE</h3>
919
  <ol style="font-family: 'Comic Neue', cursive; color: #1F2937; font-weight: 700;">
920
  <li>Upload an image or video containing faces</li>
921
  <li>Adjust detection settings (confidence, IoU, expansion)</li>
 
926
  </div>
927
 
928
  <div style="background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%); border: 3px solid #F59E0B; border-radius: 12px; padding: 1.5rem; box-shadow: 6px 6px 0px #1F2937; margin-top: 1rem;">
929
+ <h3 style="font-family: 'Bangers', cursive; color: #1F2937; font-size: 1.3rem; margin-bottom: 0.5rem;">💡 TIPS</h3>
930
  <ul style="font-family: 'Comic Neue', cursive; color: #1F2937; font-weight: 700;">
931
  <li>Lower confidence = more faces detected (may include false positives)</li>
932
  <li>Higher blur intensity = stronger privacy protection</li>
 
934
  <li>Video processing may take time depending on length</li>
935
  </ul>
936
  </div>
937
+ """
938
+ )
 
 
 
 
 
 
939
 
940
+ # Event Handlers
941
+ process_img_btn.click(
942
+ fn=process_image,
943
+ inputs=[
944
+ input_image,
945
+ conf_img,
946
+ iou_img,
947
+ expand_img,
948
+ mode_img,
949
+ blur_intensity_img,
950
+ mosaic_size_img
951
+ ],
952
+ outputs=[output_image, info_log_img]
953
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954
 
955
+ process_vid_btn.click(
956
+ fn=process_video,
957
+ inputs=[
958
+ input_video,
959
+ conf_vid,
960
+ iou_vid,
961
+ expand_vid,
962
+ mode_vid,
963
+ blur_intensity_vid,
964
+ mosaic_size_vid
965
+ ],
966
+ outputs=[output_video, info_log_vid]
967
+ )
968
 
969
 
 
 
 
970
  if __name__ == "__main__":
971
+ demo.launch(css=css, ssr_mode=False)