ysharma HF Staff commited on
Commit
5021071
·
verified ·
1 Parent(s): f231103

Update app_v2.py

Browse files
Files changed (1) hide show
  1. app_v2.py +82 -79
app_v2.py CHANGED
@@ -1,18 +1,18 @@
1
  """
2
  ==========================================
3
- Image Anonymizer
4
  ==========================================
5
  """
6
 
7
  import base64
8
  import functools
9
  import io
10
- import json
11
  from pathlib import Path
12
 
13
  import gradio as gr
14
- from fastapi import File, UploadFile
15
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
 
16
  from PIL import Image
17
 
18
  from app import (
@@ -56,46 +56,11 @@ async def homepage():
56
  return FRONTEND_HTML
57
 
58
 
59
- @server.post("/api/detect")
60
- async def detect(file: UploadFile = File(...)):
61
- suffix = Path(file.filename or "").suffix.lower()
62
- if suffix not in (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"):
63
- return JSONResponse({"error": f"Unsupported image type: {suffix or '(none)'}"}, 400)
64
- try:
65
- img_bytes = await file.read()
66
- img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
67
- except Exception as e:
68
- return JSONResponse({"error": f"Could not read image: {e}"}, 400)
69
-
70
- ocr = ocr_image(img)
71
- if not ocr["text"].strip():
72
- return JSONResponse({"error": "No text detected in the image."}, 400)
73
-
74
- try:
75
- source_text, spans = run_pii_analysis(ocr["text"])
76
- except Exception as e:
77
- return JSONResponse({"error": f"PII analysis failed: {e}"}, 500)
78
-
79
- if source_text != ocr["text"]:
80
- spans = [s for s in spans if s["end"] <= len(ocr["text"])]
81
-
82
- boxes = map_spans_to_boxes(ocr["words"], spans)
83
-
84
- buf = io.BytesIO(); img.save(buf, format="PNG")
85
- data_url = "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
86
-
87
- return JSONResponse({
88
- "filename": file.filename,
89
- "image": data_url,
90
- "width": img.width, "height": img.height,
91
- "boxes": boxes,
92
- "text": ocr["text"],
93
- "spans": spans,
94
- "categories_meta": {k: {"color": v["color"], "label": v["label"]}
95
- for k, v in CATEGORIES_META.items()},
96
- })
97
-
98
-
99
  @server.get("/api/examples")
100
  async def api_examples():
101
  return JSONResponse({"examples": _list_examples()})
@@ -118,37 +83,51 @@ async def get_example(name: str, thumb: int = 0):
118
  return FileResponse(path, headers={"Cache-Control": "public, max-age=3600"})
119
 
120
 
 
 
 
121
  @server.api(name="anonymize_screenshot")
122
- def anonymize_screenshot_api(image_path: str) -> str:
123
- """Gradio API: accepts an image (via gradio_client.handle_file) and
124
- returns detected redaction boxes as JSON.
125
 
126
- Tolerant of both a plain path string and a FileData dict, since
127
- different gradio_client versions pass each.
 
 
128
  """
129
- import traceback
130
  try:
131
- if isinstance(image_path, dict):
132
- image_path = image_path.get("path") or image_path.get("url") or ""
133
- if not image_path or not isinstance(image_path, str):
134
- return json.dumps({"error": f"expected image path, got {type(image_path).__name__}"})
135
 
136
- img = Image.open(image_path).convert("RGB")
137
  ocr = ocr_image(img)
138
  if not ocr["text"].strip():
139
- return json.dumps({
140
- "width": img.width, "height": img.height,
141
- "boxes": [], "text": "", "spans": [],
142
- })
143
- _, spans = run_pii_analysis(ocr["text"])
144
  boxes = map_spans_to_boxes(ocr["words"], spans)
145
- return json.dumps({
 
 
 
 
 
 
146
  "width": img.width, "height": img.height,
147
- "boxes": boxes, "text": ocr["text"], "spans": spans,
148
- }, ensure_ascii=False)
 
 
 
 
 
 
149
  except Exception as e:
150
  traceback.print_exc()
151
- return json.dumps({"error": f"{type(e).__name__}: {e}"})
152
 
153
 
154
  # =====================================================================
@@ -160,7 +139,7 @@ FRONTEND_HTML = r"""<!DOCTYPE html>
160
  <head>
161
  <meta charset="UTF-8">
162
  <meta name="viewport" content="width=device-width,initial-scale=1">
163
- <title>Image Anonymizer</title>
164
  <link rel="preconnect" href="https://fonts.googleapis.com">
165
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
166
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Lora:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
@@ -316,7 +295,7 @@ svg{display:block;flex-shrink:0}
316
 
317
  .landing-foot{
318
  max-width:720px;margin:.5rem auto 2rem;padding:0 1.5rem;
319
- font-family:var(--mono);font-size:12px;color:var(--text3);line-height:1.7;
320
  }
321
  .landing-foot a{color:var(--text2);text-decoration:none;border-bottom:.5px dotted var(--text3)}
322
  .landing-foot a:hover{color:var(--text)}
@@ -543,7 +522,7 @@ svg{display:block;flex-shrink:0}
543
  <rect x="2" y="9" width="16" height="3" rx="0.5" fill="currentColor"/>
544
  <rect x="2" y="14" width="8" height="3" rx="0.5" fill="currentColor"/>
545
  </svg>
546
- <span class="wordmark">Image Anonymizer</span>
547
  <span class="version">v0.3 · beta</span>
548
  </div>
549
  <div class="spacer"></div>
@@ -551,8 +530,8 @@ svg{display:block;flex-shrink:0}
551
  </header>
552
 
553
  <div class="landing-content">
554
- <h1 class="headline">Redact images before you share them.</h1>
555
- <p class="subtitle">First OCR finds the text. Then OpenAI's Privacy Filter marks names, emails, phones, addresses, dates, URLs, accounts, and secrets. You approve what gets hidden before it ever leaves the page.</p>
556
 
557
  <label class="dropzone" id="dropzone">
558
  <input type="file" id="file-input" accept="image/png,image/jpeg,image/webp,image/bmp,image/tiff">
@@ -561,13 +540,13 @@ svg{display:block;flex-shrink:0}
561
  <rect x="2" y="12" width="38" height="5" rx="1" fill="currentColor"/>
562
  <rect x="2" y="21" width="18" height="5" rx="1" fill="currentColor"/>
563
  </svg>
564
- <div class="dz-text">Drop an image, paste from clipboard, or click to browse</div>
565
  <div class="dz-hint">png · jpg · webp · bmp · tiff</div>
566
  </label>
567
 
568
  <div class="example-wrap">
569
  <div class="example-head">
570
- <span class="title">try an example · click to load (wait for a few seconds for the example to load)</span>
571
  <span class="note">all content is fictitious · mock data for testing only</span>
572
  </div>
573
  <div class="example-scroll" id="example-scroll">
@@ -577,8 +556,8 @@ svg{display:block;flex-shrink:0}
577
  </div>
578
 
579
  <div class="landing-foot">
580
- Model: <b>OpenAI Privacy Filter</b> · 1.5b params, 50m active · Apache 2.0<br>
581
- OCR: Tesseract 5 · <b>Processing: server-side using gr.Server, ZEROGPU</b> · edits stay in your browser
582
  </div>
583
  </div>
584
 
@@ -591,11 +570,11 @@ svg{display:block;flex-shrink:0}
591
  <rect x="2" y="9" width="16" height="3" rx="0.5" fill="currentColor"/>
592
  <rect x="2" y="14" width="8" height="3" rx="0.5" fill="currentColor"/>
593
  </svg>
594
- <span class="wordmark">Image Anonymizer</span>
595
  </div>
596
  <span class="version" id="meta-info">—</span>
597
  <div class="spacer"></div>
598
- <button class="btn-link" onclick="resetView()">new image</button>
599
  </header>
600
 
601
  <div class="error-banner" id="error-banner"></div>
@@ -689,7 +668,17 @@ svg{display:block;flex-shrink:0}
689
  <div id="loading"><div class="spinner"></div><p>ocr → privacy filter → map to pixels</p></div>
690
  <div class="toast" id="toast"></div>
691
 
692
- <script>
 
 
 
 
 
 
 
 
 
 
693
  // ══════════════════════════════════════════════════════════════════
694
  // State
695
  // ══════════════════════════════════════════════════════════════════
@@ -778,14 +767,20 @@ async function uploadFile(file) {
778
  if (!file.type || !file.type.startsWith('image/')) { showError('please drop an image file.'); return; }
779
  document.getElementById('loading').style.display = 'flex';
780
  document.getElementById('landing').style.display = 'none';
781
- const form = new FormData(); form.append('file', file);
782
  try {
783
- const r = await fetch('/api/detect', { method:'POST', body: form });
784
- const d = await r.json();
 
 
 
 
785
  if (d.error) { showError(d.error); return; }
 
 
 
786
  await initEditor(d);
787
  } catch (e) {
788
- showError('analysis failed: ' + e.message);
789
  } finally {
790
  document.getElementById('loading').style.display = 'none';
791
  }
@@ -1291,6 +1286,14 @@ function toast(msg) {
1291
  clearTimeout(toastTimer);
1292
  toastTimer = setTimeout(() => t.classList.remove('show'), 2000);
1293
  }
 
 
 
 
 
 
 
 
1294
  </script>
1295
  </body>
1296
  </html>"""
 
1
  """
2
  ==========================================
3
+ Screenshot Anonymizer — v2 (tool revision)
4
  ==========================================
5
  """
6
 
7
  import base64
8
  import functools
9
  import io
10
+ import traceback
11
  from pathlib import Path
12
 
13
  import gradio as gr
 
14
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
15
+ from gradio.data_classes import FileData
16
  from PIL import Image
17
 
18
  from app import (
 
56
  return FRONTEND_HTML
57
 
58
 
59
+ # The /api/examples and /examples/{name} routes below serve static
60
+ # example thumbnails and originals from disk. They are plain FastAPI
61
+ # routes because they do no GPU / queued compute — they just read
62
+ # files, which is exactly the pattern the gradio.Server blog
63
+ # recommends plain @server.get for.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  @server.get("/api/examples")
65
  async def api_examples():
66
  return JSONResponse({"examples": _list_examples()})
 
83
  return FileResponse(path, headers={"Cache-Control": "public, max-age=3600"})
84
 
85
 
86
+ # The compute endpoint: goes through Gradio's queue, plays nicely with
87
+ # @spaces.GPU on ZeroGPU, and is callable by both the browser via the
88
+ # @gradio/client JS client AND by Python users via gradio_client.
89
  @server.api(name="anonymize_screenshot")
90
+ def anonymize_screenshot_api(image: FileData) -> dict:
91
+ """OCR + PII-filter an uploaded image.
 
92
 
93
+ Input: FileData from `handle_file(file)` (JS client) or
94
+ `gradio_client.handle_file(path)` (Python client).
95
+ Output: dict with the base-64 image, OCR text, detected spans,
96
+ per-span pixel boxes, and the category color/label table.
97
  """
 
98
  try:
99
+ path = image.get("path") or image.get("url") or ""
100
+ if not path:
101
+ return {"error": "expected an image file"}
 
102
 
103
+ img = Image.open(path).convert("RGB")
104
  ocr = ocr_image(img)
105
  if not ocr["text"].strip():
106
+ return {"error": "No text detected in the image."}
107
+
108
+ source_text, spans = run_pii_analysis(ocr["text"])
109
+ if source_text != ocr["text"]:
110
+ spans = [s for s in spans if s["end"] <= len(ocr["text"])]
111
  boxes = map_spans_to_boxes(ocr["words"], spans)
112
+
113
+ buf = io.BytesIO(); img.save(buf, format="PNG")
114
+ data_url = "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
115
+
116
+ return {
117
+ "filename": Path(path).name,
118
+ "image": data_url,
119
  "width": img.width, "height": img.height,
120
+ "boxes": boxes,
121
+ "text": ocr["text"],
122
+ "spans": spans,
123
+ "categories_meta": {
124
+ k: {"color": v["color"], "label": v["label"]}
125
+ for k, v in CATEGORIES_META.items()
126
+ },
127
+ }
128
  except Exception as e:
129
  traceback.print_exc()
130
+ return {"error": f"{type(e).__name__}: {e}"}
131
 
132
 
133
  # =====================================================================
 
139
  <head>
140
  <meta charset="UTF-8">
141
  <meta name="viewport" content="width=device-width,initial-scale=1">
142
+ <title>Screenshot Anonymizer</title>
143
  <link rel="preconnect" href="https://fonts.googleapis.com">
144
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
145
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Lora:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
 
295
 
296
  .landing-foot{
297
  max-width:720px;margin:.5rem auto 2rem;padding:0 1.5rem;
298
+ font-family:var(--mono);font-size:10.5px;color:var(--text3);line-height:1.7;
299
  }
300
  .landing-foot a{color:var(--text2);text-decoration:none;border-bottom:.5px dotted var(--text3)}
301
  .landing-foot a:hover{color:var(--text)}
 
522
  <rect x="2" y="9" width="16" height="3" rx="0.5" fill="currentColor"/>
523
  <rect x="2" y="14" width="8" height="3" rx="0.5" fill="currentColor"/>
524
  </svg>
525
+ <span class="wordmark">Screenshot Anonymizer</span>
526
  <span class="version">v0.3 · beta</span>
527
  </div>
528
  <div class="spacer"></div>
 
530
  </header>
531
 
532
  <div class="landing-content">
533
+ <h1 class="headline">Redact screenshots before you post them.</h1>
534
+ <p class="subtitle">OCR finds the text. The privacy filter marks names, emails, phones, addresses, dates, URLs, accounts, and secrets. You approve what gets hidden before it ever leaves the page.</p>
535
 
536
  <label class="dropzone" id="dropzone">
537
  <input type="file" id="file-input" accept="image/png,image/jpeg,image/webp,image/bmp,image/tiff">
 
540
  <rect x="2" y="12" width="38" height="5" rx="1" fill="currentColor"/>
541
  <rect x="2" y="21" width="18" height="5" rx="1" fill="currentColor"/>
542
  </svg>
543
+ <div class="dz-text">Drop a screenshot, paste from clipboard, or click to browse</div>
544
  <div class="dz-hint">png · jpg · webp · bmp · tiff</div>
545
  </label>
546
 
547
  <div class="example-wrap">
548
  <div class="example-head">
549
+ <span class="title">try an example · click to load</span>
550
  <span class="note">all content is fictitious · mock data for testing only</span>
551
  </div>
552
  <div class="example-scroll" id="example-scroll">
 
556
  </div>
557
 
558
  <div class="landing-foot">
559
+ pii: openai/privacy-filter · 1.5b params, 50m active · apache 2.0<br>
560
+ ocr: rednote-hilab/dots.ocr · 3b vlm, top-3 on olmocr-bench · edits stay in your browser
561
  </div>
562
  </div>
563
 
 
570
  <rect x="2" y="9" width="16" height="3" rx="0.5" fill="currentColor"/>
571
  <rect x="2" y="14" width="8" height="3" rx="0.5" fill="currentColor"/>
572
  </svg>
573
+ <span class="wordmark">Screenshot Anonymizer</span>
574
  </div>
575
  <span class="version" id="meta-info">—</span>
576
  <div class="spacer"></div>
577
+ <button class="btn-link" onclick="resetView()">new screenshot</button>
578
  </header>
579
 
580
  <div class="error-banner" id="error-banner"></div>
 
668
  <div id="loading"><div class="spinner"></div><p>ocr → privacy filter → map to pixels</p></div>
669
  <div class="toast" id="toast"></div>
670
 
671
+ <script type="module">
672
+ // ═══════════════════════════════════���══════════════════════════════
673
+ // Gradio JS client — talks to the queued @server.api routes so that
674
+ // requests are serialized, progress is tracked, and ZeroGPU's
675
+ // @spaces.GPU allocator gets invoked correctly. A plain fetch() to a
676
+ // FastAPI route would bypass all of that.
677
+ // ══════════════════════════════════════════════════════════════════
678
+ import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
679
+
680
+ const clientPromise = Client.connect(window.location.origin);
681
+
682
  // ══════════════════════════════════════════════════════════════════
683
  // State
684
  // ══════════════════════════════════════════════════════════════════
 
767
  if (!file.type || !file.type.startsWith('image/')) { showError('please drop an image file.'); return; }
768
  document.getElementById('loading').style.display = 'flex';
769
  document.getElementById('landing').style.display = 'none';
 
770
  try {
771
+ const client = await clientPromise;
772
+ const result = await client.predict("/anonymize_screenshot", {
773
+ image: handle_file(file),
774
+ });
775
+ // @server.api returns a dict; the client wraps outputs in result.data[]
776
+ const d = result.data[0] || {};
777
  if (d.error) { showError(d.error); return; }
778
+ // The server returns Path(path).name as `filename`; fall back to the
779
+ // original File.name for display continuity.
780
+ if (!d.filename) d.filename = file.name;
781
  await initEditor(d);
782
  } catch (e) {
783
+ showError('analysis failed: ' + (e && e.message ? e.message : e));
784
  } finally {
785
  document.getElementById('loading').style.display = 'none';
786
  }
 
1286
  clearTimeout(toastTimer);
1287
  toastTimer = setTimeout(() => t.classList.remove('show'), 2000);
1288
  }
1289
+
1290
+ // Module scripts don't expose top-level names to the global scope, but
1291
+ // several buttons in the HTML use inline onclick="foo()" handlers —
1292
+ // bridge them explicitly.
1293
+ Object.assign(window, {
1294
+ resetView, zoomStep, zoomFit, zoomReset,
1295
+ downloadImage, copyToClipboard, exportText,
1296
+ });
1297
  </script>
1298
  </body>
1299
  </html>"""