ysharma HF Staff commited on
Commit
d77dc8f
·
verified ·
1 Parent(s): 1563f41

Update app_v2.py

Browse files
Files changed (1) hide show
  1. app_v2.py +142 -72
app_v2.py CHANGED
@@ -1,26 +1,18 @@
1
  """
2
- Screenshot Anonymizer — v2 (tool revision)
3
  ==========================================
4
- Addresses critique-ui-1.txt: the v1 landing page had the classic
5
- "AI-generated SaaS" tells (gradient wordmark, centered hero, three feature
6
- boxes, soft glow, chunky purple primary button). This version is designed
7
- as a tool-in-rest-state: dense, pixel-editor chrome; mono typography for
8
- technical values; serif for the one headline; pill-style category rows;
9
- icon tool row with keyboard shortcuts; rulers, zoom mini-toolbar, status
10
- bar, and contextual tool options in the editor.
11
-
12
- Backend is imported from app.py verbatim — the only thing changing here is
13
- the frontend and the FastAPI routes that serve it.
14
  """
15
 
16
  import base64
 
17
  import io
18
  import json
19
  from pathlib import Path
20
 
21
  import gradio as gr
22
  from fastapi import File, UploadFile
23
- from fastapi.responses import HTMLResponse, JSONResponse
24
  from PIL import Image
25
 
26
  from app import (
@@ -30,6 +22,28 @@ from app import (
30
  run_pii_analysis,
31
  )
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  # =====================================================================
34
  # SERVER
35
  # =====================================================================
@@ -82,6 +96,28 @@ async def detect(file: UploadFile = File(...)):
82
  })
83
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  @server.api(name="anonymize_screenshot")
86
  def anonymize_screenshot_api(image_path: str) -> str:
87
  img = Image.open(image_path).convert("RGB")
@@ -223,32 +259,40 @@ svg{display:block;flex-shrink:0}
223
  .dz-hint{font-family:var(--mono);font-size:11px;color:var(--text3)}
224
 
225
  /* example strip */
226
- .example{
227
  border:.5px solid var(--border);border-radius:8px;
228
  background:var(--surface);overflow:hidden;
229
  }
230
  .example-head{
231
- display:grid;grid-template-columns:1fr 1fr;
232
- font-family:var(--mono);font-size:10.5px;
233
- color:var(--text3);text-transform:lowercase;
234
- }
235
- .example-head > div{padding:.45rem .75rem;border-bottom:.5px solid var(--border)}
236
- .example-head > div + div{border-left:.5px solid var(--border)}
237
- .example-body{display:grid;grid-template-columns:1fr 1fr;min-height:120px}
238
- .example-body > div{padding:.85rem 1rem}
239
- .example-body > div + div{border-left:.5px solid var(--border)}
240
- .ex-msg{font-size:12.5px;line-height:1.55;color:var(--text)}
241
- .ex-msg .who{font-weight:500;margin-bottom:.2rem}
242
- .ex-msg .line{color:var(--text2)}
243
- .ex-msg .line + .line{margin-top:.15rem}
244
- .ex-bar{
245
- display:inline-block;background:var(--text);vertical-align:baseline;
246
- border-radius:1px;height:.95em;transform:translateY(2px);
247
- }
248
- .example-foot{
249
- padding:.4rem .75rem;border-top:.5px solid var(--border);
250
  font-family:var(--mono);font-size:10.5px;color:var(--text3);
251
- background:var(--surface2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  }
253
 
254
  .landing-foot{
@@ -342,24 +386,27 @@ svg{display:block;flex-shrink:0}
342
  }
343
 
344
  /* tool row */
345
- .tools{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin-bottom:.6rem}
346
  .tool{
347
- position:relative;
348
- display:flex;flex-direction:column;align-items:center;gap:3px;
349
- padding:8px 0 6px;
350
- border:.5px solid var(--border);border-radius:5px;
351
  color:var(--text2);
352
  transition:color .12s, background .12s, border-color .12s;
353
  }
354
- .tool:hover:not(:disabled){color:var(--text);border-color:var(--border-strong)}
355
  .tool.active{background:var(--accent-dim);border-color:var(--accent);color:var(--accent)}
356
- .tool:disabled{opacity:.35;cursor:not-allowed}
357
- .tool svg{width:15px;height:15px}
 
 
 
358
  .tool .sc{
359
- font-family:var(--mono);font-size:9.5px;color:var(--text3);
360
- letter-spacing:.02em;
 
361
  }
362
- .tool.active .sc{color:var(--accent)}
363
 
364
  /* contextual tool options */
365
  .tool-opts{
@@ -499,25 +546,14 @@ svg{display:block;flex-shrink:0}
499
  <div class="dz-hint">png · jpg · webp · bmp · tiff</div>
500
  </label>
501
 
502
- <div class="example">
503
- <div class="example-head"><div>before</div><div>after</div></div>
504
- <div class="example-body">
505
- <div>
506
- <div class="ex-msg">
507
- <div class="who">Alice Chen</div>
508
- <div class="line">send the invoice to alice@acme.com</div>
509
- <div class="line">or text me at (415) 555-0182</div>
510
- </div>
511
- </div>
512
- <div>
513
- <div class="ex-msg">
514
- <div class="who"><span class="ex-bar" style="width:84px"></span></div>
515
- <div class="line">send the invoice to <span class="ex-bar" style="width:128px"></span></div>
516
- <div class="line">or text me at <span class="ex-bar" style="width:96px"></span></div>
517
- </div>
518
- </div>
519
  </div>
520
- <div class="example-foot">example · mock chat message</div>
521
  </div>
522
  </div>
523
 
@@ -586,21 +622,15 @@ svg{display:block;flex-shrink:0}
586
  <div class="p-label">Tool</div>
587
  <div class="tools">
588
  <button class="tool active" data-mode="select" title="Select (V)">
589
- <svg viewBox="0 0 16 16"><path fill="currentColor" d="M3 2l10 5.5-4.5 1.5L7 13.5z"/></svg>
 
590
  <span class="sc">V</span>
591
  </button>
592
  <button class="tool" data-mode="draw" title="Draw bar (B)">
593
- <svg viewBox="0 0 16 16"><rect x="2" y="7" width="12" height="2" rx=".5" fill="currentColor"/><path fill="none" stroke="currentColor" stroke-width="1" d="M2 4.5h12M2 11.5h12"/></svg>
 
594
  <span class="sc">B</span>
595
  </button>
596
- <button class="tool" data-mode="eyedropper" title="Eyedropper — coming soon" disabled>
597
- <svg viewBox="0 0 16 16"><path fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M10 3l3 3M11.5 4.5L6 10l-2 .5L3 13l.5-1L9 6.5"/></svg>
598
- <span class="sc">I</span>
599
- </button>
600
- <button class="tool" data-mode="crop" title="Crop — coming soon" disabled>
601
- <svg viewBox="0 0 16 16"><path fill="none" stroke="currentColor" stroke-width="1.2" d="M4 1v11h11M1 4h11v11"/></svg>
602
- <span class="sc">C</span>
603
- </button>
604
  </div>
605
  <div class="tool-opts" id="tool-opts"></div>
606
  </section>
@@ -685,6 +715,46 @@ document.addEventListener('paste', ev => {
685
  }
686
  });
687
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
  async function uploadFile(file) {
689
  if (!file.type || !file.type.startsWith('image/')) { showError('please drop an image file.'); return; }
690
  document.getElementById('loading').style.display = 'flex';
 
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 (
 
22
  run_pii_analysis,
23
  )
24
 
25
+ EXAMPLES_DIR = Path(__file__).parent / "example-images"
26
+ EXAMPLE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
27
+
28
+
29
+ def _list_examples() -> list[str]:
30
+ if not EXAMPLES_DIR.is_dir():
31
+ return []
32
+ return sorted(p.name for p in EXAMPLES_DIR.iterdir()
33
+ if p.is_file() and p.suffix.lower() in EXAMPLE_EXTS)
34
+
35
+
36
+ @functools.lru_cache(maxsize=64)
37
+ def _example_thumbnail(name: str) -> bytes:
38
+ """Downscaled preview for the landing grid. Cached after first hit."""
39
+ path = EXAMPLES_DIR / name
40
+ img = Image.open(path).convert("RGB")
41
+ img.thumbnail((480, 480))
42
+ buf = io.BytesIO()
43
+ img.save(buf, format="JPEG", quality=82, optimize=True)
44
+ return buf.getvalue()
45
+
46
+
47
  # =====================================================================
48
  # SERVER
49
  # =====================================================================
 
96
  })
97
 
98
 
99
+ @server.get("/api/examples")
100
+ async def api_examples():
101
+ return JSONResponse({"examples": _list_examples()})
102
+
103
+
104
+ @server.get("/examples/{name}")
105
+ async def get_example(name: str, thumb: int = 0):
106
+ safe = Path(name).name
107
+ if Path(safe).suffix.lower() not in EXAMPLE_EXTS:
108
+ return JSONResponse({"error": "invalid file type"}, 400)
109
+ path = EXAMPLES_DIR / safe
110
+ if not path.is_file():
111
+ return JSONResponse({"error": "not found"}, 404)
112
+ if thumb:
113
+ return Response(
114
+ content=_example_thumbnail(safe),
115
+ media_type="image/jpeg",
116
+ headers={"Cache-Control": "public, max-age=86400"},
117
+ )
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
  img = Image.open(image_path).convert("RGB")
 
259
  .dz-hint{font-family:var(--mono);font-size:11px;color:var(--text3)}
260
 
261
  /* example strip */
262
+ .example-wrap{
263
  border:.5px solid var(--border);border-radius:8px;
264
  background:var(--surface);overflow:hidden;
265
  }
266
  .example-head{
267
+ display:flex;align-items:center;justify-content:space-between;gap:.75rem;
268
+ padding:.5rem .8rem;border-bottom:.5px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  font-family:var(--mono);font-size:10.5px;color:var(--text3);
270
+ }
271
+ .example-head .title{color:var(--text2)}
272
+ .example-head .note{color:var(--text3)}
273
+ .example-scroll{
274
+ display:flex;gap:8px;padding:10px;overflow-x:auto;
275
+ scrollbar-width:thin;scrollbar-color:var(--border-strong) transparent;
276
+ }
277
+ .example-scroll::-webkit-scrollbar{height:6px}
278
+ .example-scroll::-webkit-scrollbar-track{background:transparent}
279
+ .example-scroll::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:3px}
280
+ .ex-thumb{
281
+ flex-shrink:0;width:108px;height:108px;
282
+ border:.5px solid var(--border);border-radius:5px;overflow:hidden;
283
+ background:var(--surface2);cursor:pointer;position:relative;
284
+ transition:border-color .12s, transform .12s;
285
+ }
286
+ .ex-thumb:hover{border-color:var(--accent);transform:translateY(-1px)}
287
+ .ex-thumb img{width:100%;height:100%;object-fit:cover;display:block}
288
+ .ex-thumb .ex-idx{
289
+ position:absolute;top:4px;left:5px;
290
+ font-family:var(--mono);font-size:9.5px;color:#fff;
291
+ background:rgba(0,0,0,.55);padding:1px 4px;border-radius:2px;
292
+ letter-spacing:.02em;
293
+ }
294
+ .ex-empty{
295
+ padding:1rem;font-family:var(--mono);font-size:11px;color:var(--text3);
296
  }
297
 
298
  .landing-foot{
 
386
  }
387
 
388
  /* tool row */
389
+ .tools{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin-bottom:.7rem}
390
  .tool{
391
+ display:flex;align-items:center;gap:.55rem;
392
+ padding:10px 11px;
393
+ border:.5px solid var(--border);border-radius:6px;
 
394
  color:var(--text2);
395
  transition:color .12s, background .12s, border-color .12s;
396
  }
397
+ .tool:hover{color:var(--text);border-color:var(--border-strong)}
398
  .tool.active{background:var(--accent-dim);border-color:var(--accent);color:var(--accent)}
399
+ .tool svg{width:18px;height:18px}
400
+ .tool .name{
401
+ flex:1;text-align:left;
402
+ font-family:var(--sans);font-size:13px;font-weight:500;letter-spacing:-.005em;
403
+ }
404
  .tool .sc{
405
+ font-family:var(--mono);font-size:10.5px;color:var(--text3);
406
+ padding:1px 5px;border:.5px solid var(--border);border-radius:3px;
407
+ background:var(--bg);
408
  }
409
+ .tool.active .sc{color:var(--accent);border-color:var(--accent);background:transparent}
410
 
411
  /* contextual tool options */
412
  .tool-opts{
 
546
  <div class="dz-hint">png · jpg · webp · bmp · tiff</div>
547
  </label>
548
 
549
+ <div class="example-wrap">
550
+ <div class="example-head">
551
+ <span class="title">try an example · click to load</span>
552
+ <span class="note">all content is fictitious · mock data for testing only</span>
553
+ </div>
554
+ <div class="example-scroll" id="example-scroll">
555
+ <div class="ex-empty">loading examples…</div>
 
 
 
 
 
 
 
 
 
 
556
  </div>
 
557
  </div>
558
  </div>
559
 
 
622
  <div class="p-label">Tool</div>
623
  <div class="tools">
624
  <button class="tool active" data-mode="select" title="Select (V)">
625
+ <svg viewBox="0 0 18 18"><path fill="currentColor" d="M3 2l11 6-5 1.6L7.5 15z"/></svg>
626
+ <span class="name">Select</span>
627
  <span class="sc">V</span>
628
  </button>
629
  <button class="tool" data-mode="draw" title="Draw bar (B)">
630
+ <svg viewBox="0 0 18 18"><rect x="2" y="8" width="14" height="2.5" rx=".5" fill="currentColor"/><path fill="none" stroke="currentColor" stroke-width="1" stroke-dasharray="1.5 1.5" d="M2 5h14M2 13h14"/></svg>
631
+ <span class="name">Draw</span>
632
  <span class="sc">B</span>
633
  </button>
 
 
 
 
 
 
 
 
634
  </div>
635
  <div class="tool-opts" id="tool-opts"></div>
636
  </section>
 
715
  }
716
  });
717
 
718
+ async function loadExamples() {
719
+ const scroll = document.getElementById('example-scroll');
720
+ try {
721
+ const r = await fetch('/api/examples');
722
+ const d = await r.json();
723
+ const list = d.examples || [];
724
+ if (!list.length) {
725
+ scroll.innerHTML = '<div class="ex-empty">no examples available</div>';
726
+ return;
727
+ }
728
+ scroll.innerHTML = '';
729
+ list.forEach((name, i) => {
730
+ const el = document.createElement('div');
731
+ el.className = 'ex-thumb';
732
+ el.title = 'load ' + name;
733
+ el.innerHTML = `<span class="ex-idx">${String(i + 1).padStart(2, '0')}</span>
734
+ <img loading="lazy" src="/examples/${encodeURIComponent(name)}?thumb=1" alt="">`;
735
+ el.addEventListener('click', () => loadExample(name));
736
+ scroll.appendChild(el);
737
+ });
738
+ } catch (e) {
739
+ scroll.innerHTML = '<div class="ex-empty">could not load examples</div>';
740
+ }
741
+ }
742
+
743
+ async function loadExample(name) {
744
+ try {
745
+ const r = await fetch('/examples/' + encodeURIComponent(name));
746
+ if (!r.ok) throw new Error('fetch failed');
747
+ const blob = await r.blob();
748
+ const type = blob.type || 'image/png';
749
+ const file = new File([blob], name, { type });
750
+ uploadFile(file);
751
+ } catch (e) {
752
+ showError('could not load example: ' + e.message);
753
+ }
754
+ }
755
+
756
+ loadExamples();
757
+
758
  async function uploadFile(file) {
759
  if (!file.type || !file.type.startsWith('image/')) { showError('please drop an image file.'); return; }
760
  document.getElementById('loading').style.display = 'flex';