gheinrich commited on
Commit
bfecd26
·
1 Parent(s): 522c6a9

Add prompt safety filter and make guards mandatory

Browse files

Run Nemotron 3.5 Content Safety on the prompt before generation (fail-closed
input filter) in addition to the post-generation image check. Remove the UI
toggles so both safety filters always run; env-var opt-outs remain for local dev.

Files changed (2) hide show
  1. app.py +38 -7
  2. image_guard.py +45 -1
app.py CHANGED
@@ -159,6 +159,8 @@ IMAGE_GUARD_THRESHOLD = float(
159
  IMAGE_GUARD_OFFLOAD_T2I = os.getenv("IMAGE_GUARD_OFFLOAD_T2I", "0") == "1"
160
  # Opt-out: guard runs by default; set ENABLE_IMAGE_GUARD=0 or uncheck the UI box to disable.
161
  DEFAULT_ENABLE_IMAGE_GUARD = os.getenv("ENABLE_IMAGE_GUARD", "1") == "1"
 
 
162
 
163
  GUARD_ACCESS_HELP = (
164
  "https://huggingface.co/nvidia/Nemotron-3.5-Content-Safety"
@@ -384,6 +386,25 @@ class T2IEngine:
384
  if IMAGE_GUARD_OFFLOAD_T2I:
385
  self._reload_t2i_to_device()
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  def generate(
388
  self,
389
  prompt: str,
@@ -401,9 +422,18 @@ class T2IEngine:
401
  seed: int,
402
  micro_cond: str,
403
  return_animation: bool,
404
- enable_image_guard: bool,
 
405
  progress: gr.Progress | None = None,
406
  ):
 
 
 
 
 
 
 
 
407
  if progress is not None and self._model is None:
408
  progress(0.0, desc="Loading model (first run, this can take 1-2 min)…")
409
  with _suppress_tqdm_tracking():
@@ -461,6 +491,8 @@ class T2IEngine:
461
  f"sch_temp={schedule_temp} | conf={confidence_policy} | "
462
  f"edit_threshold={edit_threshold:.3f} | gen_time={latency:.2f}s"
463
  )
 
 
464
 
465
  if progress is not None and enable_image_guard:
466
  progress(1.0, desc="Running safety filter…")
@@ -498,7 +530,6 @@ def generate(
498
  seed: int,
499
  micro_cond: str,
500
  return_animation: bool,
501
- enable_image_guard: bool,
502
  progress: gr.Progress = gr.Progress(track_tqdm=True),
503
  ):
504
  return engine.generate(
@@ -517,7 +548,8 @@ def generate(
517
  seed,
518
  micro_cond,
519
  return_animation,
520
- enable_image_guard,
 
521
  progress=progress,
522
  )
523
 
@@ -611,9 +643,9 @@ def build_demo() -> gr.Blocks:
611
  label="Seed (-1 for random)", value=42, precision=0, scale=3
612
  )
613
  randomize_seed_btn = gr.Button("🎲 Randomize", scale=1)
614
- enable_image_guard = gr.Checkbox(
615
- label="NSFW output filter (Nemotron 3.5 Content Safety) uncheck to opt out",
616
- value=DEFAULT_ENABLE_IMAGE_GUARD,
617
  )
618
  generate_btn = gr.Button("Generate", variant="primary")
619
 
@@ -681,7 +713,6 @@ def build_demo() -> gr.Blocks:
681
  seed,
682
  micro_cond,
683
  return_animation,
684
- enable_image_guard,
685
  ],
686
  outputs=[output_image, output_meta],
687
  )
 
159
  IMAGE_GUARD_OFFLOAD_T2I = os.getenv("IMAGE_GUARD_OFFLOAD_T2I", "0") == "1"
160
  # Opt-out: guard runs by default; set ENABLE_IMAGE_GUARD=0 or uncheck the UI box to disable.
161
  DEFAULT_ENABLE_IMAGE_GUARD = os.getenv("ENABLE_IMAGE_GUARD", "1") == "1"
162
+ # Pre-generation prompt safety check (input filter), same content-safety model.
163
+ DEFAULT_ENABLE_PROMPT_GUARD = os.getenv("ENABLE_PROMPT_GUARD", "1") == "1"
164
 
165
  GUARD_ACCESS_HELP = (
166
  "https://huggingface.co/nvidia/Nemotron-3.5-Content-Safety"
 
386
  if IMAGE_GUARD_OFFLOAD_T2I:
387
  self._reload_t2i_to_device()
388
 
389
+ def _moderate_prompt(self, prompt: str) -> tuple[bool, str]:
390
+ """Run pre-generation prompt moderation. Returns (ok, meta_or_error)."""
391
+ try:
392
+ with _suppress_tqdm_tracking():
393
+ guard = self._get_image_guard()
394
+ check = guard.check_text(prompt)
395
+ if not check.passed:
396
+ message = (
397
+ "Prompt blocked by content-safety filter "
398
+ f"(label={check.label})."
399
+ )
400
+ _report_guard_failure(message)
401
+ return False, message
402
+ return True, "prompt_" + _format_guard_meta(check)
403
+ except Exception as exc:
404
+ message = _guard_unavailable_message(exc)
405
+ _report_guard_failure(message)
406
+ return False, message
407
+
408
  def generate(
409
  self,
410
  prompt: str,
 
422
  seed: int,
423
  micro_cond: str,
424
  return_animation: bool,
425
+ enable_image_guard: bool = DEFAULT_ENABLE_IMAGE_GUARD,
426
+ enable_prompt_guard: bool = DEFAULT_ENABLE_PROMPT_GUARD,
427
  progress: gr.Progress | None = None,
428
  ):
429
+ prompt_guard_meta = ""
430
+ if enable_prompt_guard:
431
+ if progress is not None:
432
+ progress(0.0, desc="Checking prompt…")
433
+ prompt_ok, prompt_guard_meta = self._moderate_prompt(prompt)
434
+ if not prompt_ok:
435
+ return None, f"ERROR: {prompt_guard_meta}"
436
+
437
  if progress is not None and self._model is None:
438
  progress(0.0, desc="Loading model (first run, this can take 1-2 min)…")
439
  with _suppress_tqdm_tracking():
 
491
  f"sch_temp={schedule_temp} | conf={confidence_policy} | "
492
  f"edit_threshold={edit_threshold:.3f} | gen_time={latency:.2f}s"
493
  )
494
+ if prompt_guard_meta:
495
+ meta += " | " + prompt_guard_meta
496
 
497
  if progress is not None and enable_image_guard:
498
  progress(1.0, desc="Running safety filter…")
 
530
  seed: int,
531
  micro_cond: str,
532
  return_animation: bool,
 
533
  progress: gr.Progress = gr.Progress(track_tqdm=True),
534
  ):
535
  return engine.generate(
 
548
  seed,
549
  micro_cond,
550
  return_animation,
551
+ enable_image_guard=DEFAULT_ENABLE_IMAGE_GUARD,
552
+ enable_prompt_guard=DEFAULT_ENABLE_PROMPT_GUARD,
553
  progress=progress,
554
  )
555
 
 
643
  label="Seed (-1 for random)", value=42, precision=0, scale=3
644
  )
645
  randomize_seed_btn = gr.Button("🎲 Randomize", scale=1)
646
+ gr.Markdown(
647
+ "Safety filters (Nemotron 3.5 Content Safety) run on both the "
648
+ "prompt and the generated image and cannot be disabled."
649
  )
650
  generate_btn = gr.Button("Generate", variant="primary")
651
 
 
713
  seed,
714
  micro_cond,
715
  return_animation,
 
716
  ],
717
  outputs=[output_image, output_meta],
718
  )
image_guard.py CHANGED
@@ -3,7 +3,7 @@
3
  #
4
  # Adapted from NVIDIA asset-harvester (asset_harvester/utils/image_guard.py).
5
 
6
- """Image moderation utility backed by Nemotron 3.5 Content Safety."""
7
 
8
  from __future__ import annotations
9
 
@@ -117,6 +117,50 @@ class ImageGuard:
117
  inference_seconds=inference_seconds,
118
  )
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  def _generate_response(self, image: Image.Image) -> str:
121
  messages = [
122
  {
 
3
  #
4
  # Adapted from NVIDIA asset-harvester (asset_harvester/utils/image_guard.py).
5
 
6
+ """Image and prompt moderation utility backed by Nemotron 3.5 Content Safety."""
7
 
8
  from __future__ import annotations
9
 
 
117
  inference_seconds=inference_seconds,
118
  )
119
 
120
+ def check_text(self, prompt: str) -> ImageGuardResult:
121
+ """Moderate a text prompt before generation (fail-closed input filter)."""
122
+ self._load()
123
+ start_time = time.perf_counter()
124
+ text = self._generate_text_response(prompt)
125
+ inference_seconds = time.perf_counter() - start_time
126
+ label, score = self._parse_response(text)
127
+ return ImageGuardResult(
128
+ passed=label == "safe",
129
+ score=score,
130
+ label=label,
131
+ raw_response=text,
132
+ model_id=self.model_id,
133
+ inference_seconds=inference_seconds,
134
+ )
135
+
136
+ def _generate_text_response(self, prompt: str) -> str:
137
+ messages = [
138
+ {
139
+ "role": "user",
140
+ "content": [{"type": "text", "text": prompt}],
141
+ }
142
+ ]
143
+ inputs = self._processor.apply_chat_template(
144
+ messages,
145
+ add_generation_prompt=True,
146
+ tokenize=True,
147
+ return_dict=True,
148
+ return_tensors="pt",
149
+ request_categories="/categories",
150
+ enable_thinking=False,
151
+ ).to(self.device)
152
+
153
+ input_len = inputs["input_ids"].shape[-1]
154
+ with torch.inference_mode():
155
+ generation = self._model.generate(
156
+ **inputs,
157
+ max_new_tokens=_MAX_NEW_TOKENS,
158
+ do_sample=False,
159
+ )
160
+ generation = generation[0][input_len:]
161
+
162
+ return self._processor.decode(generation, skip_special_tokens=True).strip()
163
+
164
  def _generate_response(self, image: Image.Image) -> str:
165
  messages = [
166
  {