Spaces:
Running on Zero
Running on Zero
Add prompt safety filter and make guards mandatory
Browse filesRun 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.
- app.py +38 -7
- 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 |
-
|
| 615 |
-
|
| 616 |
-
|
| 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 |
{
|