a-01a commited on
Commit
0faa034
Β·
verified Β·
1 Parent(s): 6c33365

Deploy FaceCloak

Browse files
README.md CHANGED
@@ -10,7 +10,9 @@ app_file: app.py
10
  suggested_hardware: cpu-basic
11
  ---
12
 
13
- # πŸ›‘οΈ FaceCloak β€” Adversarial Biometric Privacy
 
 
14
 
15
  **Upload your photo. Watch AI become blind to your face.**
16
 
@@ -36,9 +38,9 @@ The following table demonstrates the drop in recognition confidence on standardi
36
 
37
  | Strength (Ξ΅) | Steps | Original Similarity | Cloaked Similarity | Result |
38
  |--------------|-------|---------------------|--------------------|--------|
39
- | 0.01 (Weak) | 50 | 0.999 | 0.654 | ⚠️ Matched |
40
- | 0.03 (Normal)| 100 | 0.999 | 0.128 | βœ… Cloaked |
41
- | 0.05 (Strong)| 200 | 0.999 | -0.215 | βœ… Cloaked |
42
 
43
  ## Limitations & Ethical Considerations
44
 
@@ -60,3 +62,8 @@ uv run python app.py
60
  uv run pytest -v # unit tests
61
  uv run pytest -v -m integration # tests with real portrait images
62
  ```
 
 
 
 
 
 
10
  suggested_hardware: cpu-basic
11
  ---
12
 
13
+ # FaceCloak β€” Adversarial Biometric Privacy
14
+
15
+ [**Live Demo on Hugging Face Spaces**](https://huggingface.co/spaces/a-01a/facecloak)
16
 
17
  **Upload your photo. Watch AI become blind to your face.**
18
 
 
38
 
39
  | Strength (Ξ΅) | Steps | Original Similarity | Cloaked Similarity | Result |
40
  |--------------|-------|---------------------|--------------------|--------|
41
+ | 0.01 (Weak) | 50 | 0.999 | 0.654 | WARNING |
42
+ | 0.03 (Normal)| 100 | 0.999 | 0.128 | SUCCESS |
43
+ | 0.05 (Strong)| 200 | 0.999 | -0.215 | SUCCESS |
44
 
45
  ## Limitations & Ethical Considerations
46
 
 
62
  uv run pytest -v # unit tests
63
  uv run pytest -v -m integration # tests with real portrait images
64
  ```
65
+
66
+ ## License
67
+
68
+ MIT License
69
+ Copyright (c) 990aa
app.py CHANGED
@@ -3,4 +3,3 @@ from facecloak.interface import APP_CSS, APP_THEME, demo
3
 
4
  if __name__ == "__main__":
5
  demo.launch(theme=APP_THEME, css=APP_CSS)
6
-
 
3
 
4
  if __name__ == "__main__":
5
  demo.launch(theme=APP_THEME, css=APP_CSS)
 
facecloak/__pycache__/deploy.cpython-312.pyc CHANGED
Binary files a/facecloak/__pycache__/deploy.cpython-312.pyc and b/facecloak/__pycache__/deploy.cpython-312.pyc differ
 
facecloak/cloaking.py CHANGED
@@ -23,7 +23,7 @@ from facecloak.pipeline import (
23
  @dataclass(frozen=True, slots=True)
24
  class CloakHyperparameters:
25
  epsilon: float = 0.03
26
- alpha_fraction: float = 0.1 # alpha = alpha_fraction * epsilon
27
  num_steps: int = 100
28
  l2_lambda: float = 0.01
29
 
@@ -86,9 +86,7 @@ def cloak_face_tensor(
86
 
87
  model = model or get_embedding_model()
88
  original_batch = (
89
- _prepare_face_batch(face_tensor)
90
- .to(next(model.parameters()).device)
91
- .detach()
92
  )
93
  original_embedding = model(original_batch).detach()
94
  delta = torch.zeros_like(original_batch, requires_grad=True)
@@ -104,7 +102,9 @@ def cloak_face_tensor(
104
 
105
  cloaked_batch = torch.clamp(original_batch + delta, DISPLAY_MIN, DISPLAY_MAX)
106
  cloaked_embedding = model(cloaked_batch)
107
- cosine = F.cosine_similarity(original_embedding, cloaked_embedding, dim=1).mean()
 
 
108
 
109
  objective = -cosine - parameters.l2_lambda * delta.pow(2).mean()
110
  objective.backward()
@@ -113,7 +113,8 @@ def cloak_face_tensor(
113
  delta.add_(alpha * delta.grad.sign())
114
  delta.clamp_(-parameters.epsilon, parameters.epsilon)
115
  delta.copy_(
116
- torch.clamp(original_batch + delta, DISPLAY_MIN, DISPLAY_MAX) - original_batch
 
117
  )
118
 
119
  # Optimization (Step 38): `.item()` blocks the execution stream. Only evaluate
@@ -139,7 +140,9 @@ def cloak_face_tensor(
139
  original_face_image=standardized_tensor_to_pil(original_cpu),
140
  cloaked_face_image=standardized_tensor_to_pil(final_cpu),
141
  perturbation_preview=perturbation_preview_image(delta_cpu),
142
- amplified_diff=amplified_diff_image(original_cpu[0], final_cpu[0], amplification=75.0),
 
 
143
  cloaked_face_tensor=final_cpu[0],
144
  delta_tensor=delta_cpu[0],
145
  original_similarity=1.0,
 
23
  @dataclass(frozen=True, slots=True)
24
  class CloakHyperparameters:
25
  epsilon: float = 0.03
26
+ alpha_fraction: float = 0.1 # alpha = alpha_fraction * epsilon
27
  num_steps: int = 100
28
  l2_lambda: float = 0.01
29
 
 
86
 
87
  model = model or get_embedding_model()
88
  original_batch = (
89
+ _prepare_face_batch(face_tensor).to(next(model.parameters()).device).detach()
 
 
90
  )
91
  original_embedding = model(original_batch).detach()
92
  delta = torch.zeros_like(original_batch, requires_grad=True)
 
102
 
103
  cloaked_batch = torch.clamp(original_batch + delta, DISPLAY_MIN, DISPLAY_MAX)
104
  cloaked_embedding = model(cloaked_batch)
105
+ cosine = F.cosine_similarity(
106
+ original_embedding, cloaked_embedding, dim=1
107
+ ).mean()
108
 
109
  objective = -cosine - parameters.l2_lambda * delta.pow(2).mean()
110
  objective.backward()
 
113
  delta.add_(alpha * delta.grad.sign())
114
  delta.clamp_(-parameters.epsilon, parameters.epsilon)
115
  delta.copy_(
116
+ torch.clamp(original_batch + delta, DISPLAY_MIN, DISPLAY_MAX)
117
+ - original_batch
118
  )
119
 
120
  # Optimization (Step 38): `.item()` blocks the execution stream. Only evaluate
 
140
  original_face_image=standardized_tensor_to_pil(original_cpu),
141
  cloaked_face_image=standardized_tensor_to_pil(final_cpu),
142
  perturbation_preview=perturbation_preview_image(delta_cpu),
143
+ amplified_diff=amplified_diff_image(
144
+ original_cpu[0], final_cpu[0], amplification=75.0
145
+ ),
146
  cloaked_face_tensor=final_cpu[0],
147
  delta_tensor=delta_cpu[0],
148
  original_similarity=1.0,
facecloak/deploy.py CHANGED
@@ -65,7 +65,9 @@ def default_space_repo_id(api: HfApi, token: str) -> str:
65
  whoami = api.whoami(token=token, cache=False)
66
  username = whoami.get("name")
67
  if not username:
68
- raise FaceCloakError("Could not determine the Hugging Face username from the provided token.")
 
 
69
  return f"{username}/{PROJECT_SLUG}"
70
 
71
 
 
65
  whoami = api.whoami(token=token, cache=False)
66
  username = whoami.get("name")
67
  if not username:
68
+ raise FaceCloakError(
69
+ "Could not determine the Hugging Face username from the provided token."
70
+ )
71
  return f"{username}/{PROJECT_SLUG}"
72
 
73
 
facecloak/interface.py CHANGED
@@ -2,9 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
5
- import io
6
  import tempfile
7
- from pathlib import Path
8
 
9
  import gradio as gr
10
 
@@ -18,7 +16,7 @@ from facecloak.pipeline import (
18
  interpret_score,
19
  verify_cloak,
20
  )
21
- from facecloak.project import PROJECT_NAME, PROJECT_TAGLINE
22
 
23
  # ---------------------------------------------------------------------------
24
  # CSS β€” dark-on-light theme with explicit text colours for HF Spaces compat
@@ -33,28 +31,6 @@ APP_CSS = """
33
 
34
  body, .gradio-container {
35
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
36
- color: #0f172a !important;
37
- background: #f8fafc !important;
38
- }
39
-
40
- /* ── Background gradient ──────────────────────────────────────── */
41
- .gradio-container {
42
- background:
43
- radial-gradient(ellipse 80% 60% at 10% 0%, rgba(99, 102, 241, 0.08) 0%, transparent 55%),
44
- radial-gradient(ellipse 60% 50% at 90% 5%, rgba(244, 114, 182, 0.07) 0%, transparent 50%),
45
- linear-gradient(160deg, #f0f4ff 0%, #faf5ff 45%, #fff7f0 100%) !important;
46
- min-height: 100vh;
47
- }
48
-
49
- /* ── Force dark text everywhere ──────────────────────────────── */
50
- label, .label-wrap, .block > label,
51
- span, p, h1, h2, h3, h4, h5, h6,
52
- .markdown, .markdown p, .markdown li,
53
- .markdown h1, .markdown h2, .markdown h3,
54
- .svelte-1ed2p3z, .svelte-1gfkn6j,
55
- textarea, input, .gr-textbox textarea,
56
- .gr-number input, .gr-slider label {
57
- color: #0f172a !important;
58
  }
59
 
60
  /* ── Hero section ────────────────────────────────────────────── */
@@ -73,32 +49,40 @@ textarea, input, .gr-textbox textarea,
73
 
74
  /* ── Panel cards ─────────────────────────────────────────────── */
75
  .panel {
76
- background: rgba(255, 255, 255, 0.85) !important;
77
  border: 1px solid rgba(99, 102, 241, 0.12) !important;
78
  border-radius: 16px !important;
79
  box-shadow: 0 4px 24px rgba(99, 102, 241, 0.07) !important;
80
  padding: 1rem 1.25rem !important;
81
- color: #0f172a !important;
82
  }
83
  .panel *, .panel .markdown, .panel .markdown p, .panel .markdown li {
84
  color: #0f172a !important;
85
  }
86
 
87
- /* ── Accordion ───────────────────────────────────────────────── */
88
- .accordion, details, details summary {
89
- background: rgba(248, 250, 252, 0.9) !important;
90
- color: #334155 !important;
91
  border-radius: 12px !important;
 
 
 
 
 
92
  }
93
  details *, details .markdown, details .markdown p {
94
- color: #334155 !important;
95
  }
96
 
97
- /* ── Sliders ─────────────────────────────────────────────────── */
98
- .gr-slider label, input[type=range] + span,
99
- .gr-slider .label-wrap span {
100
- color: #334155 !important;
101
- font-weight: 500;
 
 
 
 
 
102
  }
103
 
104
  /* ── Buttons ─────────────────────────────────────────────────── */
@@ -145,24 +129,18 @@ details *, details .markdown, details .markdown p {
145
  border-radius: 10px !important;
146
  color: #0f172a !important;
147
  border: 1px solid rgba(99, 102, 241, 0.2) !important;
148
- background: rgba(255,255,255,0.9) !important;
149
- }
150
-
151
- /* ── Images ──────────────────────────────────────────────────── */
152
- .image-container img {
153
- border-radius: 12px !important;
154
- box-shadow: 0 4px 20px rgba(0,0,0,0.1) !important;
155
  }
156
 
157
  /* ── Tab strip ───────────────────────────────────────────────── */
158
- .tab-nav button {
159
- color: #475569 !important;
160
- font-weight: 500 !important;
 
161
  }
162
- .tab-nav button.selected {
163
  color: #4f46e5 !important;
164
  border-bottom-color: #4f46e5 !important;
165
- font-weight: 600 !important;
166
  }
167
  """
168
 
@@ -179,6 +157,7 @@ APP_THEME = gr.themes.Base(
179
  # Score formatting helpers
180
  # ---------------------------------------------------------------------------
181
 
 
182
  def _pct(similarity: float) -> str:
183
  return f"{max(0.0, min(100.0, similarity * 100.0)):.1f}%"
184
 
@@ -196,6 +175,7 @@ def _format_detection_probability(probability: float | None) -> str:
196
  # Core processing β€” generator so Gradio streams progress (Step 28)
197
  # ---------------------------------------------------------------------------
198
 
 
199
  def generate_cloak(image, epsilon, num_steps, alpha_fraction):
200
  """Generator function that yields intermediate UI state every PGD step.
201
 
@@ -293,13 +273,13 @@ def generate_cloak(image, epsilon, num_steps, alpha_fraction):
293
  chunk = acc.lines[i : i + chunk_size]
294
  status_so_far += "\n".join(chunk) + "\n"
295
  yield (
296
- None, # orig_img – not yet
297
- None, # cloaked_img – not yet
298
- None, # diff_img – not yet
299
  orig_score_text,
300
  "Optimizing…",
301
  status_so_far,
302
- None, # download path – not yet
303
  )
304
 
305
  # ── Post-cloak verification (Step 21) ─────────────────────────────── #
@@ -321,7 +301,7 @@ def generate_cloak(image, epsilon, num_steps, alpha_fraction):
321
  warning_msg = ""
322
  if final_sim > 0.5:
323
  warning_msg = (
324
- "\n⚠️ Partial cloak achieved. "
325
  "Try increasing the number of steps or the epsilon value."
326
  )
327
 
@@ -338,7 +318,7 @@ def generate_cloak(image, epsilon, num_steps, alpha_fraction):
338
 
339
  # ── Final yield with all outputs filled in ────────────────────────── #
340
  yield (
341
- detected.image, # original aligned face
342
  result.cloaked_face_image,
343
  result.amplified_diff,
344
  orig_score_text,
@@ -352,10 +332,13 @@ def generate_cloak(image, epsilon, num_steps, alpha_fraction):
352
  # Compare-faces tab (kept for technical reviewers)
353
  # ---------------------------------------------------------------------------
354
 
 
355
  def compare_faces(image_a, image_b):
356
  try:
357
  if image_a is None or image_b is None:
358
- raise FaceCloakError("Please provide both images before running a similarity check.")
 
 
359
 
360
  detected_a = detect_primary_face(image_a)
361
  detected_b = detect_primary_face(image_b)
@@ -381,13 +364,13 @@ def compare_faces(image_a, image_b):
381
  # Build the Gradio app
382
  # ---------------------------------------------------------------------------
383
 
 
384
  def build_demo() -> gr.Blocks: # noqa: C901
385
  with gr.Blocks(title=PROJECT_NAME) as demo:
386
-
387
  # ── Hero ─────────────────────────────────────────────────────── #
388
  gr.Markdown(
389
- f"""
390
- # πŸ›‘οΈ FaceCloak
391
  ## Upload your photo. Watch AI become blind to your face.
392
 
393
  This tool uses adversarial mathematics to make microscopic pixel changes that are invisible to humans but completely scramble AI facial recognition systems.
@@ -397,9 +380,8 @@ This tool uses adversarial mathematics to make microscopic pixel changes that ar
397
 
398
  # ── Tabs ─────────────────────────────────────────────────────── #
399
  with gr.Tab("Generate Cloak"):
400
-
401
  # ── Input section ─────────────────────────────────────────── #
402
- gr.Markdown("### πŸ“Έ Step 1 β€” Upload Your Photo", elem_classes=["panel"])
403
 
404
  with gr.Row():
405
  with gr.Column(scale=1):
@@ -415,7 +397,7 @@ This tool uses adversarial mathematics to make microscopic pixel changes that ar
415
  )
416
 
417
  with gr.Column(scale=1):
418
- gr.Markdown("### βš™οΈ Step 2 β€” Adjust Settings (optional)")
419
 
420
  epsilon = gr.Slider(
421
  minimum=0.01,
@@ -442,7 +424,7 @@ This tool uses adversarial mathematics to make microscopic pixel changes that ar
442
  info="How aggressively each step adjusts pixels. 0.1 is a good default.",
443
  )
444
 
445
- with gr.Accordion("πŸ”¬ What do these settings mean?", open=False):
446
  gr.Markdown(
447
  """
448
  **Perturbation Strength (epsilon):** Think of this as the maximum number of steps
@@ -461,7 +443,7 @@ The defaults work well for most photos. Only adjust if you are experimenting.
461
 
462
  # ── Trigger ───────────────────────────────────────────────── #
463
  cloak_btn = gr.Button(
464
- "⚑ Generate Cloak",
465
  variant="primary",
466
  elem_id="cloak-btn",
467
  )
@@ -476,7 +458,7 @@ The defaults work well for most photos. Only adjust if you are experimenting.
476
  )
477
 
478
  # ── Results section ───────────────────────────────────────── #
479
- gr.Markdown("### 🎯 Results", elem_classes=["panel"])
480
 
481
  with gr.Row():
482
  with gr.Column(scale=1):
@@ -514,7 +496,7 @@ The defaults work well for most photos. Only adjust if you are experimenting.
514
 
515
  with gr.Row(elem_id="download-btn"):
516
  download_btn = gr.DownloadButton(
517
- label="⬇️ Download Cloaked Image",
518
  variant="secondary",
519
  )
520
 
@@ -543,10 +525,16 @@ The defaults work well for most photos. Only adjust if you are experimenting.
543
  compare_image_a = gr.Image(label="Photo A", type="pil")
544
  compare_image_b = gr.Image(label="Photo B", type="pil")
545
  with gr.Row():
546
- aligned_a = gr.Image(label="Detected Face A", type="pil", interactive=False)
547
- aligned_b = gr.Image(label="Detected Face B", type="pil", interactive=False)
 
 
 
 
548
  compare_button = gr.Button("Compare Faces", variant="primary")
549
- pair_similarity = gr.Number(label="Cosine Similarity (1.0 = identical, 0 = unrelated)", precision=4)
 
 
550
  pair_summary = gr.Markdown(elem_classes=["panel"])
551
  compare_button.click(
552
  fn=compare_faces,
 
2
 
3
  from __future__ import annotations
4
 
 
5
  import tempfile
 
6
 
7
  import gradio as gr
8
 
 
16
  interpret_score,
17
  verify_cloak,
18
  )
19
+ from facecloak.project import PROJECT_NAME
20
 
21
  # ---------------------------------------------------------------------------
22
  # CSS β€” dark-on-light theme with explicit text colours for HF Spaces compat
 
31
 
32
  body, .gradio-container {
33
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
 
36
  /* ── Hero section ────────────────────────────────────────────── */
 
49
 
50
  /* ── Panel cards ─────────────────────────────────────────────── */
51
  .panel {
52
+ background: #ffffff !important;
53
  border: 1px solid rgba(99, 102, 241, 0.12) !important;
54
  border-radius: 16px !important;
55
  box-shadow: 0 4px 24px rgba(99, 102, 241, 0.07) !important;
56
  padding: 1rem 1.25rem !important;
 
57
  }
58
  .panel *, .panel .markdown, .panel .markdown p, .panel .markdown li {
59
  color: #0f172a !important;
60
  }
61
 
62
+ /* ── Accordion (What do these settings mean?) ────────────────── */
63
+ .accordion, details {
64
+ background: #1e293b !important;
 
65
  border-radius: 12px !important;
66
+ border: 1px solid rgba(255,255,255,0.1) !important;
67
+ }
68
+ details summary, details summary * {
69
+ color: #f8fafc !important;
70
+ font-weight: 600 !important;
71
  }
72
  details *, details .markdown, details .markdown p {
73
+ color: #e2e8f0 !important;
74
  }
75
 
76
+ /* ── Sliders and settings ────────────────────────────────────── */
77
+ .gr-slider label, .gr-slider .label-wrap span, input[type=range] + span {
78
+ color: #f8fafc !important;
79
+ font-weight: 600 !important;
80
+ }
81
+
82
+ .gr-slider {
83
+ background: #1e293b !important;
84
+ padding: 1rem !important;
85
+ border-radius: 12px !important;
86
  }
87
 
88
  /* ── Buttons ─────────────────────────────────────────────────── */
 
129
  border-radius: 10px !important;
130
  color: #0f172a !important;
131
  border: 1px solid rgba(99, 102, 241, 0.2) !important;
132
+ background: #ffffff !important;
 
 
 
 
 
 
133
  }
134
 
135
  /* ── Tab strip ───────────────────────────────────────────────── */
136
+ button[role="tab"] {
137
+ color: #1e293b !important;
138
+ font-weight: 600 !important;
139
+ font-size: 1.05rem !important;
140
  }
141
+ button[role="tab"][aria-selected="true"] {
142
  color: #4f46e5 !important;
143
  border-bottom-color: #4f46e5 !important;
 
144
  }
145
  """
146
 
 
157
  # Score formatting helpers
158
  # ---------------------------------------------------------------------------
159
 
160
+
161
  def _pct(similarity: float) -> str:
162
  return f"{max(0.0, min(100.0, similarity * 100.0)):.1f}%"
163
 
 
175
  # Core processing β€” generator so Gradio streams progress (Step 28)
176
  # ---------------------------------------------------------------------------
177
 
178
+
179
  def generate_cloak(image, epsilon, num_steps, alpha_fraction):
180
  """Generator function that yields intermediate UI state every PGD step.
181
 
 
273
  chunk = acc.lines[i : i + chunk_size]
274
  status_so_far += "\n".join(chunk) + "\n"
275
  yield (
276
+ None, # orig_img – not yet
277
+ None, # cloaked_img – not yet
278
+ None, # diff_img – not yet
279
  orig_score_text,
280
  "Optimizing…",
281
  status_so_far,
282
+ None, # download path – not yet
283
  )
284
 
285
  # ── Post-cloak verification (Step 21) ─────────────────────────────── #
 
301
  warning_msg = ""
302
  if final_sim > 0.5:
303
  warning_msg = (
304
+ "\nWARNING: Partial cloak achieved. "
305
  "Try increasing the number of steps or the epsilon value."
306
  )
307
 
 
318
 
319
  # ── Final yield with all outputs filled in ────────────────────────── #
320
  yield (
321
+ detected.image, # original aligned face
322
  result.cloaked_face_image,
323
  result.amplified_diff,
324
  orig_score_text,
 
332
  # Compare-faces tab (kept for technical reviewers)
333
  # ---------------------------------------------------------------------------
334
 
335
+
336
  def compare_faces(image_a, image_b):
337
  try:
338
  if image_a is None or image_b is None:
339
+ raise FaceCloakError(
340
+ "Please provide both images before running a similarity check."
341
+ )
342
 
343
  detected_a = detect_primary_face(image_a)
344
  detected_b = detect_primary_face(image_b)
 
364
  # Build the Gradio app
365
  # ---------------------------------------------------------------------------
366
 
367
+
368
  def build_demo() -> gr.Blocks: # noqa: C901
369
  with gr.Blocks(title=PROJECT_NAME) as demo:
 
370
  # ── Hero ─────────────────────────────────────────────────────── #
371
  gr.Markdown(
372
+ """
373
+ # <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline; vertical-align:middle; margin-right:8px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg> FaceCloak
374
  ## Upload your photo. Watch AI become blind to your face.
375
 
376
  This tool uses adversarial mathematics to make microscopic pixel changes that are invisible to humans but completely scramble AI facial recognition systems.
 
380
 
381
  # ── Tabs ─────────────────────────────────────────────────────── #
382
  with gr.Tab("Generate Cloak"):
 
383
  # ── Input section ─────────────────────────────────────────── #
384
+ gr.Markdown("### <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:inline; vertical-align:middle; margin-right:8px;\"><path d=\"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z\"></path><circle cx=\"12\" cy=\"13\" r=\"4\"></circle></svg> Step 1 β€” Upload Your Photo", elem_classes=["panel"])
385
 
386
  with gr.Row():
387
  with gr.Column(scale=1):
 
397
  )
398
 
399
  with gr.Column(scale=1):
400
+ gr.Markdown("### <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:inline; vertical-align:middle; margin-right:8px;\"><circle cx=\"12\" cy=\"12\" r=\"3\"></circle><path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z\"></path></svg> Step 2 β€” Adjust Settings (optional)")
401
 
402
  epsilon = gr.Slider(
403
  minimum=0.01,
 
424
  info="How aggressively each step adjusts pixels. 0.1 is a good default.",
425
  )
426
 
427
+ with gr.Accordion("What do these settings mean?", open=False):
428
  gr.Markdown(
429
  """
430
  **Perturbation Strength (epsilon):** Think of this as the maximum number of steps
 
443
 
444
  # ── Trigger ───────────────────────────────────────────────── #
445
  cloak_btn = gr.Button(
446
+ "Generate Cloak",
447
  variant="primary",
448
  elem_id="cloak-btn",
449
  )
 
458
  )
459
 
460
  # ── Results section ───────────────────────────────────────── #
461
+ gr.Markdown("### <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:inline; vertical-align:middle; margin-right:8px;\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><circle cx=\"12\" cy=\"12\" r=\"6\"></circle><circle cx=\"12\" cy=\"12\" r=\"2\"></circle></svg> Results", elem_classes=["panel"])
462
 
463
  with gr.Row():
464
  with gr.Column(scale=1):
 
496
 
497
  with gr.Row(elem_id="download-btn"):
498
  download_btn = gr.DownloadButton(
499
+ label="Download Cloaked Image",
500
  variant="secondary",
501
  )
502
 
 
525
  compare_image_a = gr.Image(label="Photo A", type="pil")
526
  compare_image_b = gr.Image(label="Photo B", type="pil")
527
  with gr.Row():
528
+ aligned_a = gr.Image(
529
+ label="Detected Face A", type="pil", interactive=False
530
+ )
531
+ aligned_b = gr.Image(
532
+ label="Detected Face B", type="pil", interactive=False
533
+ )
534
  compare_button = gr.Button("Compare Faces", variant="primary")
535
+ pair_similarity = gr.Number(
536
+ label="Cosine Similarity (1.0 = identical, 0 = unrelated)", precision=4
537
+ )
538
  pair_summary = gr.Markdown(elem_classes=["panel"])
539
  compare_button.click(
540
  fn=compare_faces,
facecloak/models.py CHANGED
@@ -41,7 +41,7 @@ def get_embedding_model() -> InceptionResnetV1:
41
  parameter.requires_grad_(False)
42
  return model
43
 
 
44
  # Step 37: Optimize initialization (instantiate at module load time)
45
  get_face_detector()
46
  get_embedding_model()
47
-
 
41
  parameter.requires_grad_(False)
42
  return model
43
 
44
+
45
  # Step 37: Optimize initialization (instantiate at module load time)
46
  get_face_detector()
47
  get_embedding_model()
 
facecloak/pipeline.py CHANGED
@@ -118,7 +118,9 @@ def amplified_diff_image(
118
  return Image.fromarray(arr, mode="RGB")
119
 
120
 
121
- def detect_primary_face(image: Image.Image, detector: Any | None = None) -> DetectedFace:
 
 
122
  """Detect and align the largest face, with input-size guard (Steps 30, 34)."""
123
  detector = detector or get_face_detector()
124
  rgb_image = ensure_rgb(resize_for_detection(image))
@@ -190,13 +192,13 @@ def interpret_score(similarity: float) -> tuple[str, str | None]:
190
  ``None`` when the cloak fully succeeded; a guidance string otherwise.
191
  """
192
  if similarity > 0.7:
193
- label = "⚠️ Face Matched β€” AI still recognizes you"
194
  warning = "Partial cloak only. Try increasing the number of steps or epsilon."
195
  elif similarity > 0.3:
196
- label = "πŸ”Ά Partial Cloak β€” recognition weakened"
197
  warning = "Try increasing the number of steps or the epsilon value for a stronger cloak."
198
  else:
199
- label = "βœ… Identity Cloaked β€” AI cannot recognize you"
200
  warning = None
201
  return label, warning
202
 
 
118
  return Image.fromarray(arr, mode="RGB")
119
 
120
 
121
+ def detect_primary_face(
122
+ image: Image.Image, detector: Any | None = None
123
+ ) -> DetectedFace:
124
  """Detect and align the largest face, with input-size guard (Steps 30, 34)."""
125
  detector = detector or get_face_detector()
126
  rgb_image = ensure_rgb(resize_for_detection(image))
 
192
  ``None`` when the cloak fully succeeded; a guidance string otherwise.
193
  """
194
  if similarity > 0.7:
195
+ label = "WARNING: Face Matched β€” AI still recognizes you"
196
  warning = "Partial cloak only. Try increasing the number of steps or epsilon."
197
  elif similarity > 0.3:
198
+ label = "PARTIAL: Partial Cloak β€” recognition weakened"
199
  warning = "Try increasing the number of steps or the epsilon value for a stronger cloak."
200
  else:
201
+ label = "SUCCESS: Identity Cloaked β€” AI cannot recognize you"
202
  warning = None
203
  return label, warning
204
 
tests/test_deploy.py CHANGED
@@ -47,7 +47,9 @@ def _local_temp_dir(name: str) -> Path:
47
  def test_read_env_file_parses_simple_key_values() -> None:
48
  temp_dir = _local_temp_dir("env")
49
  env_path = temp_dir / ".env"
50
- env_path.write_text(f"{HF_TOKEN_ENV_VAR}=hf_test_token\nOTHER=value\n", encoding="utf-8")
 
 
51
 
52
  values = read_env_file(env_path)
53
 
 
47
  def test_read_env_file_parses_simple_key_values() -> None:
48
  temp_dir = _local_temp_dir("env")
49
  env_path = temp_dir / ".env"
50
+ env_path.write_text(
51
+ f"{HF_TOKEN_ENV_VAR}=hf_test_token\nOTHER=value\n", encoding="utf-8"
52
+ )
53
 
54
  values = read_env_file(env_path)
55
 
tests/test_integration.py CHANGED
@@ -75,4 +75,3 @@ def test_verify_cloak_uses_fresh_mtcnn_pass_on_real_image() -> None:
75
  # A strong cloak can push cosine similarity negative (anti-correlated vectors).
76
  # pct = similarity * 100 and is NOT clamped β€” that is intentional.
77
  assert verification.pct == pytest.approx(verification.similarity * 100.0, abs=0.01)
78
-
 
75
  # A strong cloak can push cosine similarity negative (anti-correlated vectors).
76
  # pct = similarity * 100 and is NOT clamped β€” that is intentional.
77
  assert verification.pct == pytest.approx(verification.similarity * 100.0, abs=0.01)
 
tests/test_interface.py CHANGED
@@ -48,9 +48,11 @@ def test_interface_has_no_phase_references() -> None:
48
  # The demo object doesn't expose raw text easily; check the source module
49
  import inspect
50
  from facecloak import interface
 
51
  source = inspect.getsource(interface)
52
  # Should not contain "Phase 2", "Phase 3" etc. in user-facing strings
53
  import re
54
- matches = re.findall(r'\bPhase [0-9]\b', source)
 
55
  # Allow zero matches
56
  assert len(matches) == 0, f"Found phase number references in interface: {matches}"
 
48
  # The demo object doesn't expose raw text easily; check the source module
49
  import inspect
50
  from facecloak import interface
51
+
52
  source = inspect.getsource(interface)
53
  # Should not contain "Phase 2", "Phase 3" etc. in user-facing strings
54
  import re
55
+
56
+ matches = re.findall(r"\bPhase [0-9]\b", source)
57
  # Allow zero matches
58
  assert len(matches) == 0, f"Found phase number references in interface: {matches}"
tests/test_pipeline.py CHANGED
@@ -10,7 +10,6 @@ import torch
10
  from facecloak.errors import FaceCloakError
11
  from facecloak.pipeline import (
12
  MAX_INPUT_DIMENSION,
13
- MIN_CROP_DIMENSION,
14
  amplified_diff_image,
15
  cosine_similarity,
16
  detect_primary_face,
@@ -54,8 +53,11 @@ class DummyEmbeddingModel(torch.nn.Module):
54
 
55
  # ── detect_primary_face ─────────────────────────────────────────────────── #
56
 
 
57
  def test_detect_primary_face_returns_probability_and_image() -> None:
58
- detected = detect_primary_face(Image.new("RGB", (64, 64), "white"), detector=DummyDetector())
 
 
59
 
60
  assert detected.tensor.shape == (3, 160, 160)
61
  assert detected.probability == pytest.approx(0.97)
@@ -64,11 +66,14 @@ def test_detect_primary_face_returns_probability_and_image() -> None:
64
 
65
  def test_detect_primary_face_raises_readable_error_when_no_face_found() -> None:
66
  with pytest.raises(FaceCloakError, match="No face detected"):
67
- detect_primary_face(Image.new("RGB", (64, 64), "white"), detector=MissingFaceDetector())
 
 
68
 
69
 
70
  # ── resize_for_detection (Step 34) ──────────────────────────────────────── #
71
 
 
72
  def test_resize_for_detection_downsizes_large_images() -> None:
73
  big = Image.new("RGB", (2000, 1500), "white")
74
  resized = resize_for_detection(big)
@@ -90,6 +95,7 @@ def test_resize_for_detection_preserves_aspect_ratio() -> None:
90
 
91
  # ── Embedding extraction ─────────────────────────────────────────────────── #
92
 
 
93
  def test_extract_embedding_tensor_preserves_gradients() -> None:
94
  face_tensor = torch.ones(3, 4, 4, requires_grad=True)
95
  model = DummyEmbeddingModel()
@@ -103,6 +109,7 @@ def test_extract_embedding_tensor_preserves_gradients() -> None:
103
 
104
  # ── cosine_similarity ────────────────────────────────────────────────────── #
105
 
 
106
  def test_cosine_similarity_behaves_for_identical_and_opposite_vectors() -> None:
107
  same = cosine_similarity(np.array([1.0, 2.0]), np.array([1.0, 2.0]))
108
  opposite = cosine_similarity(np.array([1.0, 0.0]), np.array([-1.0, 0.0]))
@@ -113,6 +120,7 @@ def test_cosine_similarity_behaves_for_identical_and_opposite_vectors() -> None:
113
 
114
  # ── Visualization helpers ────────────────────────────────────────────────── #
115
 
 
116
  def test_tensor_visualization_helpers_return_pil_images() -> None:
117
  face_tensor = torch.zeros(3, 160, 160)
118
  face_image = standardized_tensor_to_pil(face_tensor)
@@ -143,6 +151,7 @@ def test_amplified_diff_image_amplifies_differences() -> None:
143
 
144
  # ── interpret_score (Step 22) ────────────────────────────────────────────── #
145
 
 
146
  def test_interpret_score_high_similarity_returns_matched_label() -> None:
147
  label, warning = interpret_score(0.85)
148
  assert "Matched" in label
@@ -163,12 +172,14 @@ def test_interpret_score_mid_range_returns_partial_label() -> None:
163
 
164
  # ── verify_cloak (Step 21) ───────────────────────────────────────────────── #
165
 
 
166
  def test_verify_cloak_uses_fresh_detection_not_original_tensor() -> None:
167
  """verify_cloak must run MTCNN on the cloaked PIL image, not reuse tensors."""
168
  import torch.nn.functional as F
169
 
170
  class SizeAgnosticModel(torch.nn.Module):
171
  """Returns L2-normalized mean of flattened tensor, works for any HΓ—W."""
 
172
  device = torch.device("cpu")
173
 
174
  def forward(self, batch: torch.Tensor) -> torch.Tensor:
@@ -179,6 +190,7 @@ def test_verify_cloak_uses_fresh_detection_not_original_tensor() -> None:
179
 
180
  class NonZeroDetector:
181
  """Returns a non-zero face tensor so embedding is always non-zero."""
 
182
  def __call__(self, _image, return_prob=False):
183
  face_tensor = torch.ones(3, 160, 160) * 0.3 # ~38/255 grey
184
  if return_prob:
@@ -202,5 +214,3 @@ def test_verify_cloak_uses_fresh_detection_not_original_tensor() -> None:
202
  assert hasattr(result, "label")
203
  assert hasattr(result, "pct")
204
  assert isinstance(result.label, str) and len(result.label) > 0
205
-
206
-
 
10
  from facecloak.errors import FaceCloakError
11
  from facecloak.pipeline import (
12
  MAX_INPUT_DIMENSION,
 
13
  amplified_diff_image,
14
  cosine_similarity,
15
  detect_primary_face,
 
53
 
54
  # ── detect_primary_face ─────────────────────────────────────────────────── #
55
 
56
+
57
  def test_detect_primary_face_returns_probability_and_image() -> None:
58
+ detected = detect_primary_face(
59
+ Image.new("RGB", (64, 64), "white"), detector=DummyDetector()
60
+ )
61
 
62
  assert detected.tensor.shape == (3, 160, 160)
63
  assert detected.probability == pytest.approx(0.97)
 
66
 
67
  def test_detect_primary_face_raises_readable_error_when_no_face_found() -> None:
68
  with pytest.raises(FaceCloakError, match="No face detected"):
69
+ detect_primary_face(
70
+ Image.new("RGB", (64, 64), "white"), detector=MissingFaceDetector()
71
+ )
72
 
73
 
74
  # ── resize_for_detection (Step 34) ──────────────────────────────────────── #
75
 
76
+
77
  def test_resize_for_detection_downsizes_large_images() -> None:
78
  big = Image.new("RGB", (2000, 1500), "white")
79
  resized = resize_for_detection(big)
 
95
 
96
  # ── Embedding extraction ─────────────────────────────────────────────────── #
97
 
98
+
99
  def test_extract_embedding_tensor_preserves_gradients() -> None:
100
  face_tensor = torch.ones(3, 4, 4, requires_grad=True)
101
  model = DummyEmbeddingModel()
 
109
 
110
  # ── cosine_similarity ────────────────────────────────────────────────────── #
111
 
112
+
113
  def test_cosine_similarity_behaves_for_identical_and_opposite_vectors() -> None:
114
  same = cosine_similarity(np.array([1.0, 2.0]), np.array([1.0, 2.0]))
115
  opposite = cosine_similarity(np.array([1.0, 0.0]), np.array([-1.0, 0.0]))
 
120
 
121
  # ── Visualization helpers ────────────────────────────────────────────────── #
122
 
123
+
124
  def test_tensor_visualization_helpers_return_pil_images() -> None:
125
  face_tensor = torch.zeros(3, 160, 160)
126
  face_image = standardized_tensor_to_pil(face_tensor)
 
151
 
152
  # ── interpret_score (Step 22) ────────────────────────────────────────────── #
153
 
154
+
155
  def test_interpret_score_high_similarity_returns_matched_label() -> None:
156
  label, warning = interpret_score(0.85)
157
  assert "Matched" in label
 
172
 
173
  # ── verify_cloak (Step 21) ───────────────────────────────────────────────── #
174
 
175
+
176
  def test_verify_cloak_uses_fresh_detection_not_original_tensor() -> None:
177
  """verify_cloak must run MTCNN on the cloaked PIL image, not reuse tensors."""
178
  import torch.nn.functional as F
179
 
180
  class SizeAgnosticModel(torch.nn.Module):
181
  """Returns L2-normalized mean of flattened tensor, works for any HΓ—W."""
182
+
183
  device = torch.device("cpu")
184
 
185
  def forward(self, batch: torch.Tensor) -> torch.Tensor:
 
190
 
191
  class NonZeroDetector:
192
  """Returns a non-zero face tensor so embedding is always non-zero."""
193
+
194
  def __call__(self, _image, return_prob=False):
195
  face_tensor = torch.ones(3, 160, 160) * 0.3 # ~38/255 grey
196
  if return_prob:
 
214
  assert hasattr(result, "label")
215
  assert hasattr(result, "pct")
216
  assert isinstance(result.label, str) and len(result.label) > 0
 
 
tests/test_project.py CHANGED
@@ -18,6 +18,7 @@ def test_requirements_file_matches_pinned_runtime_dependencies() -> None:
18
  def test_phase_label_is_not_a_numbered_phase() -> None:
19
  """PHASE_LABEL must not contain a phase number reference."""
20
  import re
21
- assert not re.search(r'\bPhase [0-9]\b', PHASE_LABEL), (
 
22
  f"PHASE_LABEL should not contain a numbered phase: {PHASE_LABEL!r}"
23
  )
 
18
  def test_phase_label_is_not_a_numbered_phase() -> None:
19
  """PHASE_LABEL must not contain a phase number reference."""
20
  import re
21
+
22
+ assert not re.search(r"\bPhase [0-9]\b", PHASE_LABEL), (
23
  f"PHASE_LABEL should not contain a numbered phase: {PHASE_LABEL!r}"
24
  )