fcakyon commited on
Commit
de117b4
·
verified ·
1 Parent(s): 588c1d0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +34 -121
app.py CHANGED
@@ -8,45 +8,22 @@ from io import BytesIO
8
  from PIL import Image
9
  from functools import lru_cache
10
 
11
- # Load environment variables from a .env file
12
  load_dotenv()
13
 
14
- # --- Secure Token Management ---
15
- # Get the Hugging Face token from environment variables
16
  VIDDEXA_TOKEN = os.getenv("HF_TOKEN")
17
 
18
- # A simple cache to store loaded model instances
19
  _MODEL_CACHE: Dict[str, Any] = {}
20
 
21
- # --- CSS Styles ---
22
- # Custom CSS for the verdict card
23
  CUSTOM_CSS = """
24
  .verdict-safe { background-color: #D5F5E3; border: 2px solid #2ECC71; color: #1D8348; }
25
  .verdict-sensitive { background-color: #FCF3CF; border: 2px solid #F1C40F; color: #B7950B; }
26
  .verdict-nsfw { background-color: #FADBD8; border: 2px solid #E74C3C; color: #B03A2E; }
27
  .verdict-card { padding: 20px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold; }
28
-
29
- /* Responsive Galleries: show mobile (2 cols) on small screens, desktop (4 cols) on larger */
30
- #examples-gallery-mobile { display: block; }
31
- #examples-gallery-desktop { display: none; }
32
- @media (min-width: 900px) {
33
- #examples-gallery-mobile { display: none; }
34
- #examples-gallery-desktop { display: block; }
35
- }
36
-
37
- /* Ensure the Viddexa logo is readable on dark backgrounds */
38
- .viddexa-logo {
39
- background-color: #FFFFFF; /* white background behind the SVG */
40
- padding: 8px; /* a little breathing room */
41
- border-radius: 8px; /* soften the corners */
42
- }
43
-
44
  footer {visibility: hidden}
45
  """
46
 
47
 
48
- # --- Helper Functions ---
49
-
50
  @lru_cache(maxsize=32)
51
  def _download_image_bytes(image_url: str) -> bytes:
52
  req = Request(image_url, headers={"User-Agent": "viddexa-gradio-demo/1.0"})
@@ -60,7 +37,6 @@ def _load_model(model_id: str) -> Any:
60
  return _MODEL_CACHE[model_id]
61
  try:
62
  from moderators.auto_model import AutoModerator
63
- # Download the model from Hugging Face
64
  model = AutoModerator.from_pretrained(model_id, token=VIDDEXA_TOKEN, use_fast=True)
65
  _MODEL_CACHE[model_id] = model
66
  return model
@@ -81,7 +57,6 @@ def _get_image_input(image_path: str | None, image_url: str | None) -> Image.Ima
81
  except Exception as fetch_err:
82
  raise gr.Error(f"Could not download or open the image from the URL: {fetch_err}")
83
  elif image_path:
84
- # Open the image from a local file
85
  img = Image.open(image_path)
86
  return img.convert("RGB")
87
  else:
@@ -95,19 +70,15 @@ def _format_results(results: list) -> Tuple[str, Dict[str, float], str, Dict]:
95
 
96
  classifications = results[0]["classifications"]
97
 
98
- # Normalize to a dict[str, float]
99
  label_output: Dict[str, float]
100
  if isinstance(classifications, dict):
101
  label_output = {str(k): float(v) for k, v in classifications.items()}
102
  else:
103
- # Assume list[{'label': str, 'score': float}] shape
104
  try:
105
  label_output = {str(item['label']): float(item['score']) for item in classifications}
106
  except Exception:
107
- # Fallback to empty if unexpected
108
  label_output = {}
109
 
110
- # Determine the final verdict
111
  scores = {label.lower(): score for label, score in label_output.items()}
112
  nsfw_score = scores.get("nsfw", 0.0)
113
 
@@ -123,7 +94,6 @@ def _format_results(results: list) -> Tuple[str, Dict[str, float], str, Dict]:
123
 
124
  verdict_html = f"<div class='verdict-card {verdict_class}'>{verdict_text}</div>"
125
 
126
- # Prepare scores for Markdown list
127
  markdown_output = "### All Scores\n---\n"
128
  for label, score in sorted(label_output.items(), key=lambda kv: kv[1], reverse=True):
129
  markdown_output += f"- **{label.capitalize()}**: {score:.4f}\n"
@@ -131,42 +101,27 @@ def _format_results(results: list) -> Tuple[str, Dict[str, float], str, Dict]:
131
  return verdict_html, label_output, markdown_output, results[0]
132
 
133
 
134
- # --- Main Analysis Function ---
135
-
136
  def analyze_image(image_path: str | None, image_url: str | None, model_choice: str,
137
  progress=gr.Progress(track_tqdm=True)):
138
  """The main inference function for the Gradio interface."""
139
  progress(0, desc="Initializing Analysis...")
140
-
141
- # 1. Get Image Input
142
  progress(0.2, desc="Processing Image...")
143
  input_image = _get_image_input(image_path, image_url)
144
-
145
- # 2. Load Model
146
  progress(0.5, desc=f"Loading Model: {os.path.basename(model_choice)}...")
147
  model = _load_model(model_choice)
148
-
149
- # 3. Run Inference
150
  progress(0.8, desc="Running Inference...")
151
  results = model(input_image)
152
 
153
- # Helper to make model outputs JSON-serializable and in expected shape
154
  json_results = [
155
  {"classifications": getattr(r, "classifications", r)}
156
  for r in results
157
  ]
158
  json_results = json.loads(json.dumps(json_results, ensure_ascii=False))
159
 
160
- # 4. Format and Return Results
161
  progress(1, desc="Complete!")
162
  return _format_results(json_results)
163
 
164
 
165
- def analyze_image_from_url(image_url: str, model_choice: str, progress=gr.Progress(track_tqdm=True)):
166
- """Wrapper to run examples with just URL + model."""
167
- return analyze_image(None, image_url, model_choice, progress)
168
-
169
-
170
  def analyze_image_with_status(image_path: str | None, image_url: str | None, model_choice: str,
171
  progress=gr.Progress(track_tqdm=True)):
172
  """Run analysis and also return a user-friendly status string."""
@@ -180,7 +135,6 @@ def analyze_image_with_status(image_path: str | None, image_url: str | None, mod
180
  return verdict_html, label_scores, md_scores, json_obj, status
181
 
182
 
183
- # Example mapping for gallery selections
184
  EXAMPLE_ITEMS = [
185
  (
186
  "https://assets.clevelandclinic.org/transform/LargeFeatureImage/cd71f4bd-81d4-45d8-a450-74df78e4477a/Apples-184940975-770x533-1_jpg",
@@ -215,7 +169,6 @@ def run_example_by_index(evt: gr.SelectData):
215
  url, model, caption = EXAMPLE_ITEMS[idx]
216
  verdict_html, label_scores, md_scores, json_obj = analyze_image(None, url, model)
217
  status = f"Last analysed example: {caption}"
218
- # Also update the dropdown and URL textbox for transparency
219
  return (
220
  verdict_html,
221
  label_scores,
@@ -232,27 +185,26 @@ with gr.Blocks(
232
  css=CUSTOM_CSS,
233
  analytics_enabled=False,
234
  ) as demo:
235
- # Header and Introduction
236
  gr.HTML("""
237
- <div class=\"viddexa-header\" style=\"text-align: center; max-width: 800px; margin: 0 auto;\">
238
- <img class=\"viddexa-logo\" src=\"https://aky-tech.com/images/viddexa-logo.svg\" alt=\"Viddexa logo\" style=\"max-width: 320px; width: 80%; height: auto; display: block; margin: 10px auto 6px;\" />
239
- <p style=\"font-size: 1.2em; color: #888;\">
240
- Official demo for <a href=\"https://github.com/viddexa/moderators\" target=\"_blank\"><code>moderators</code></a> package.
241
  <br>
242
  Upload an image or provide a URL to get an instant content analysis.
243
  </p>
244
  <p>
245
  🔗 <b>Project Links:</b>
246
- <a href=\"https://huggingface.co/viddexa/nsfw-mini\" target=\"_blank\">[Model: nsfw-mini]</a> |
247
- <a href=\"https://huggingface.co/viddexa/nsfw-nano\" target=\"_blank\">[Model: nsfw-nano]</a> |
248
- <a href=\"https://arxiv.org/abs/2312.16338\" target=\"_blank\">[Arxiv]</a> |
249
- <a href=\"https://github.com/viddexa/moderators\" target=\"_blank\">[GitHub]</a> |
250
- <a href=\"https://pypi.org/project/moderators/\" target=\"_blank\">[PyPI]</a>
251
  </p>
252
  <p>
253
  <details style="margin-top: 10px;">
254
  <summary style="cursor: pointer; font-weight: bold;">📄 BibTeX entry for citation</summary>
255
- <pre style="background-color: rgba(128, 128, 128, 0.1); border: 1px solid rgba(128, 128, 128, 0.3); padding: 15px; border-radius: 5px; text-align: left; overflow-x: auto; margin: 10px 0;"><code style="font-family: monospace;"><code>@article{akyon2023nudity,
256
  title={State-of-the-art in nudity classification: A comparative analysis},
257
  author={Akyon, Fatih Cagatay and Temizel, Alptekin},
258
  booktitle={2023 IEEE International Conference on Acoustics, Speech, and Signal Processing Workshops (ICASSPW)},
@@ -265,9 +217,8 @@ with gr.Blocks(
265
  </div>
266
  """)
267
 
268
- with gr.Row(variant="panel") as main_row:
269
- # Capture both columns so we can add widgets in any order
270
- with gr.Column(scale=1, min_width=350) as left_col:
271
  gr.Markdown("## ⚙️ Step 1: Configure Settings")
272
  model_choice = gr.Dropdown(
273
  choices=["viddexa/nsfw-detection-mini", "viddexa/nsfw-detection-nano"],
@@ -277,10 +228,10 @@ with gr.Blocks(
277
  )
278
 
279
  gr.Markdown("## 🖼️ Step 2: Provide an Image")
280
- with gr.Tabs() as input_tabs:
281
- with gr.TabItem("Upload Image", id="upload_tab"):
282
  image_input = gr.Image(type="filepath", label="Drag & drop a file or click to upload")
283
- with gr.TabItem("From URL", id="url_tab"):
284
  image_url_input = gr.Textbox(
285
  label="Image URL",
286
  placeholder="https://example.com/image.jpg",
@@ -288,8 +239,7 @@ with gr.Blocks(
288
 
289
  run_btn = gr.Button("Start Analysis", variant="primary", scale=2)
290
 
291
- # Column 2: Outputs
292
- with gr.Column(scale=2, min_width=500) as right_col:
293
  gr.Markdown("## 📊 Step 3: Review Results")
294
  verdict_output = gr.HTML(label="Final Verdict")
295
  label_output = gr.Label(label="Classification Scores", num_top_classes=4, show_label=True)
@@ -298,80 +248,43 @@ with gr.Blocks(
298
  with gr.Accordion("Show Raw JSON Output", open=False):
299
  json_output = gr.JSON(label="Model Output (JSON)")
300
 
301
- # Full-width Examples Gallery section
302
  gr.Markdown("## 🎯 Try an Example (click an image)")
303
  gallery_items = [[url, caption] for (url, _model, caption) in EXAMPLE_ITEMS]
304
- examples_gallery_mobile = gr.Gallery(
305
  label="Try an Example",
306
  value=gallery_items,
307
- columns=2,
308
  height="auto",
309
  allow_preview=False,
310
- elem_id="examples-gallery-mobile",
311
- )
312
- examples_gallery_desktop = gr.Gallery(
313
- label=None,
314
- value=gallery_items,
315
- columns=4,
316
- height="auto",
317
- allow_preview=False,
318
- elem_id="examples-gallery-desktop",
319
  )
320
 
321
- # Status label under galleries
322
- status_md = gr.Markdown("Last analyse: —", elem_id="last-example-status")
323
-
324
- # When a gallery image is selected, run analysis and update outputs + inputs + status
325
- for gallery in (examples_gallery_mobile, examples_gallery_desktop):
326
- gallery.select(
327
- fn=run_example_by_index,
328
- outputs=[
329
- verdict_output,
330
- label_output,
331
- markdown_output,
332
- json_output,
333
- model_choice,
334
- image_url_input,
335
- status_md,
336
- ],
337
- )
338
-
339
- # Interactive Event Listeners
340
 
341
- # When the analysis button is clicked
342
  run_btn.click(
343
  fn=analyze_image_with_status,
344
  inputs=[image_input, image_url_input, model_choice],
345
  outputs=[verdict_output, label_output, markdown_output, json_output, status_md],
346
  )
347
 
348
- # Clear the other input when one changes (avoid relying on Tabs.select on some Gradio versions)
349
- def _clear_url_on_image_change(_img):
350
- # Keep image as-is, clear URL
351
- return gr.update(), gr.update(value="")
352
-
353
- def _clear_image_on_url_change(_url):
354
- # Clear uploaded image, keep URL as-is
355
- return gr.update(value=None), gr.update()
356
-
357
- image_input.change(
358
- fn=_clear_url_on_image_change,
359
- inputs=[image_input],
360
- outputs=[image_input, image_url_input],
361
- )
362
- image_url_input.change(
363
- fn=_clear_image_on_url_change,
364
- inputs=[image_url_input],
365
- outputs=[image_input, image_url_input],
366
- )
367
-
368
  gr.HTML("""
369
- <div style=\"text-align: center; margin-top: 20px; color: #888;\">
370
  <p>Developed by Viddexa.</p>
371
  </div>
372
  """)
373
 
374
- # Create an 'examples' directory and download a sample image if it doesn't exist
375
  if not os.path.exists("examples"):
376
  os.makedirs("examples")
377
  print("Created 'examples' directory.")
 
8
  from PIL import Image
9
  from functools import lru_cache
10
 
 
11
  load_dotenv()
12
 
 
 
13
  VIDDEXA_TOKEN = os.getenv("HF_TOKEN")
14
 
 
15
  _MODEL_CACHE: Dict[str, Any] = {}
16
 
 
 
17
  CUSTOM_CSS = """
18
  .verdict-safe { background-color: #D5F5E3; border: 2px solid #2ECC71; color: #1D8348; }
19
  .verdict-sensitive { background-color: #FCF3CF; border: 2px solid #F1C40F; color: #B7950B; }
20
  .verdict-nsfw { background-color: #FADBD8; border: 2px solid #E74C3C; color: #B03A2E; }
21
  .verdict-card { padding: 20px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold; }
22
+ .viddexa-logo { background-color: #FFFFFF; padding: 8px; border-radius: 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  footer {visibility: hidden}
24
  """
25
 
26
 
 
 
27
  @lru_cache(maxsize=32)
28
  def _download_image_bytes(image_url: str) -> bytes:
29
  req = Request(image_url, headers={"User-Agent": "viddexa-gradio-demo/1.0"})
 
37
  return _MODEL_CACHE[model_id]
38
  try:
39
  from moderators.auto_model import AutoModerator
 
40
  model = AutoModerator.from_pretrained(model_id, token=VIDDEXA_TOKEN, use_fast=True)
41
  _MODEL_CACHE[model_id] = model
42
  return model
 
57
  except Exception as fetch_err:
58
  raise gr.Error(f"Could not download or open the image from the URL: {fetch_err}")
59
  elif image_path:
 
60
  img = Image.open(image_path)
61
  return img.convert("RGB")
62
  else:
 
70
 
71
  classifications = results[0]["classifications"]
72
 
 
73
  label_output: Dict[str, float]
74
  if isinstance(classifications, dict):
75
  label_output = {str(k): float(v) for k, v in classifications.items()}
76
  else:
 
77
  try:
78
  label_output = {str(item['label']): float(item['score']) for item in classifications}
79
  except Exception:
 
80
  label_output = {}
81
 
 
82
  scores = {label.lower(): score for label, score in label_output.items()}
83
  nsfw_score = scores.get("nsfw", 0.0)
84
 
 
94
 
95
  verdict_html = f"<div class='verdict-card {verdict_class}'>{verdict_text}</div>"
96
 
 
97
  markdown_output = "### All Scores\n---\n"
98
  for label, score in sorted(label_output.items(), key=lambda kv: kv[1], reverse=True):
99
  markdown_output += f"- **{label.capitalize()}**: {score:.4f}\n"
 
101
  return verdict_html, label_output, markdown_output, results[0]
102
 
103
 
 
 
104
  def analyze_image(image_path: str | None, image_url: str | None, model_choice: str,
105
  progress=gr.Progress(track_tqdm=True)):
106
  """The main inference function for the Gradio interface."""
107
  progress(0, desc="Initializing Analysis...")
 
 
108
  progress(0.2, desc="Processing Image...")
109
  input_image = _get_image_input(image_path, image_url)
 
 
110
  progress(0.5, desc=f"Loading Model: {os.path.basename(model_choice)}...")
111
  model = _load_model(model_choice)
 
 
112
  progress(0.8, desc="Running Inference...")
113
  results = model(input_image)
114
 
 
115
  json_results = [
116
  {"classifications": getattr(r, "classifications", r)}
117
  for r in results
118
  ]
119
  json_results = json.loads(json.dumps(json_results, ensure_ascii=False))
120
 
 
121
  progress(1, desc="Complete!")
122
  return _format_results(json_results)
123
 
124
 
 
 
 
 
 
125
  def analyze_image_with_status(image_path: str | None, image_url: str | None, model_choice: str,
126
  progress=gr.Progress(track_tqdm=True)):
127
  """Run analysis and also return a user-friendly status string."""
 
135
  return verdict_html, label_scores, md_scores, json_obj, status
136
 
137
 
 
138
  EXAMPLE_ITEMS = [
139
  (
140
  "https://assets.clevelandclinic.org/transform/LargeFeatureImage/cd71f4bd-81d4-45d8-a450-74df78e4477a/Apples-184940975-770x533-1_jpg",
 
169
  url, model, caption = EXAMPLE_ITEMS[idx]
170
  verdict_html, label_scores, md_scores, json_obj = analyze_image(None, url, model)
171
  status = f"Last analysed example: {caption}"
 
172
  return (
173
  verdict_html,
174
  label_scores,
 
185
  css=CUSTOM_CSS,
186
  analytics_enabled=False,
187
  ) as demo:
 
188
  gr.HTML("""
189
+ <div class="viddexa-header" style="text-align: center; max-width: 800px; margin: 0 auto;">
190
+ <img class="viddexa-logo" src="https://aky-tech.com/images/viddexa-logo.svg" alt="Viddexa logo" style="max-width: 320px; width: 80%; height: auto; display: block; margin: 10px auto 6px;" />
191
+ <p style="font-size: 1.2em; color: #888;">
192
+ Official demo for <a href="https://github.com/viddexa/moderators" target="_blank"><code>moderators</code></a> package.
193
  <br>
194
  Upload an image or provide a URL to get an instant content analysis.
195
  </p>
196
  <p>
197
  🔗 <b>Project Links:</b>
198
+ <a href="https://huggingface.co/viddexa/nsfw-mini" target="_blank">[Model: nsfw-mini]</a> |
199
+ <a href="https://huggingface.co/viddexa/nsfw-nano" target="_blank">[Model: nsfw-nano]</a> |
200
+ <a href="https://arxiv.org/abs/2312.16338" target="_blank">[Arxiv]</a> |
201
+ <a href="https://github.com/viddexa/moderators" target="_blank">[GitHub]</a> |
202
+ <a href="https://pypi.org/project/moderators/" target="_blank">[PyPI]</a>
203
  </p>
204
  <p>
205
  <details style="margin-top: 10px;">
206
  <summary style="cursor: pointer; font-weight: bold;">📄 BibTeX entry for citation</summary>
207
+ <pre style="background-color: rgba(128, 128, 128, 0.1); border: 1px solid rgba(128, 128, 128, 0.3); padding: 15px; border-radius: 5px; text-align: left; overflow-x: auto; margin: 10px 0;"><code style="font-family: monospace;">@article{akyon2023nudity,
208
  title={State-of-the-art in nudity classification: A comparative analysis},
209
  author={Akyon, Fatih Cagatay and Temizel, Alptekin},
210
  booktitle={2023 IEEE International Conference on Acoustics, Speech, and Signal Processing Workshops (ICASSPW)},
 
217
  </div>
218
  """)
219
 
220
+ with gr.Row(variant="panel"):
221
+ with gr.Column(scale=1, min_width=350):
 
222
  gr.Markdown("## ⚙️ Step 1: Configure Settings")
223
  model_choice = gr.Dropdown(
224
  choices=["viddexa/nsfw-detection-mini", "viddexa/nsfw-detection-nano"],
 
228
  )
229
 
230
  gr.Markdown("## 🖼️ Step 2: Provide an Image")
231
+ with gr.Tabs():
232
+ with gr.TabItem("Upload Image"):
233
  image_input = gr.Image(type="filepath", label="Drag & drop a file or click to upload")
234
+ with gr.TabItem("From URL"):
235
  image_url_input = gr.Textbox(
236
  label="Image URL",
237
  placeholder="https://example.com/image.jpg",
 
239
 
240
  run_btn = gr.Button("Start Analysis", variant="primary", scale=2)
241
 
242
+ with gr.Column(scale=2, min_width=500):
 
243
  gr.Markdown("## 📊 Step 3: Review Results")
244
  verdict_output = gr.HTML(label="Final Verdict")
245
  label_output = gr.Label(label="Classification Scores", num_top_classes=4, show_label=True)
 
248
  with gr.Accordion("Show Raw JSON Output", open=False):
249
  json_output = gr.JSON(label="Model Output (JSON)")
250
 
 
251
  gr.Markdown("## 🎯 Try an Example (click an image)")
252
  gallery_items = [[url, caption] for (url, _model, caption) in EXAMPLE_ITEMS]
253
+ examples_gallery = gr.Gallery(
254
  label="Try an Example",
255
  value=gallery_items,
256
+ columns=[2, 4],
257
  height="auto",
258
  allow_preview=False,
 
 
 
 
 
 
 
 
 
259
  )
260
 
261
+ status_md = gr.Markdown("Last analysed: —", elem_id="last-example-status")
262
+
263
+ examples_gallery.select(
264
+ fn=run_example_by_index,
265
+ outputs=[
266
+ verdict_output,
267
+ label_output,
268
+ markdown_output,
269
+ json_output,
270
+ model_choice,
271
+ image_url_input,
272
+ status_md,
273
+ ],
274
+ )
 
 
 
 
 
275
 
 
276
  run_btn.click(
277
  fn=analyze_image_with_status,
278
  inputs=[image_input, image_url_input, model_choice],
279
  outputs=[verdict_output, label_output, markdown_output, json_output, status_md],
280
  )
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  gr.HTML("""
283
+ <div style="text-align: center; margin-top: 20px; color: #888;">
284
  <p>Developed by Viddexa.</p>
285
  </div>
286
  """)
287
 
 
288
  if not os.path.exists("examples"):
289
  os.makedirs("examples")
290
  print("Created 'examples' directory.")