Spaces:
Running on Zero
Running on Zero
[Admin] Add NCII prompt guard for image-conditioned generations
#4
by akhaliq HF Staff - opened
- app.py +17 -0
- ncii_guard.py +90 -0
app.py
CHANGED
|
@@ -80,6 +80,18 @@ LOADED_IN: float | None = None
|
|
| 80 |
LORA_STATUS: str | None = None
|
| 81 |
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
def status() -> str:
|
| 84 |
if LOAD_ERROR:
|
| 85 |
return LOAD_ERROR
|
|
@@ -318,6 +330,8 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
|
|
| 318 |
raise Exception("The denoiser is still loading.")
|
| 319 |
if not prompt or not prompt.strip():
|
| 320 |
raise Exception("MiniMax-H3 always takes a prompt, keyframes or not.")
|
|
|
|
|
|
|
| 321 |
|
| 322 |
from PIL import Image, ImageOps
|
| 323 |
|
|
@@ -438,6 +452,9 @@ def homepage():
|
|
| 438 |
return f.read()
|
| 439 |
|
| 440 |
|
|
|
|
|
|
|
|
|
|
| 441 |
load_models()
|
| 442 |
|
| 443 |
if __name__ == "__main__":
|
|
|
|
| 80 |
LORA_STATUS: str | None = None
|
| 81 |
|
| 82 |
|
| 83 |
+
def check_prompt(prompt: str) -> None:
|
| 84 |
+
"""The NCII guard, on requests carrying a keyframe — the edit-on-a-real-photo case. It runs before the
|
| 85 |
+
conditioner call and the denoise booking, so a refused prompt costs no GPU time on either half. The
|
| 86 |
+
classifier lives in `ncii_guard`'s spawned subprocess — in the main process it kills every GPU worker."""
|
| 87 |
+
import ncii_guard
|
| 88 |
+
|
| 89 |
+
flag = ncii_guard.classify(prompt)
|
| 90 |
+
if flag["label"] == "ncii":
|
| 91 |
+
print(f"[guard] prompt refused (ncii {flag['score']:.2f}): {prompt!r}", flush=True)
|
| 92 |
+
raise Exception("This prompt was flagged by a content filter and wasn't run.")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
def status() -> str:
|
| 96 |
if LOAD_ERROR:
|
| 97 |
return LOAD_ERROR
|
|
|
|
| 330 |
raise Exception("The denoiser is still loading.")
|
| 331 |
if not prompt or not prompt.strip():
|
| 332 |
raise Exception("MiniMax-H3 always takes a prompt, keyframes or not.")
|
| 333 |
+
if image_path or last_image_path:
|
| 334 |
+
check_prompt(prompt)
|
| 335 |
|
| 336 |
from PIL import Image, ImageOps
|
| 337 |
|
|
|
|
| 452 |
return f.read()
|
| 453 |
|
| 454 |
|
| 455 |
+
import ncii_guard
|
| 456 |
+
|
| 457 |
+
ncii_guard.start()
|
| 458 |
load_models()
|
| 459 |
|
| 460 |
if __name__ == "__main__":
|
ncii_guard.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The NCII prompt guard, in its own process.
|
| 2 |
+
|
| 3 |
+
[`hfmlsoc/ncii-light-guard-v01`](https://huggingface.co/hfmlsoc/ncii-light-guard-v01) is a 270M CPU text
|
| 4 |
+
classifier scoring the NCII risk of an edit prompt. It cannot live in the main process: with it loaded there,
|
| 5 |
+
every subsequent `@spaces.GPU` worker dies at `worker_init` with `RuntimeError: No CUDA GPUs are available` —
|
| 6 |
+
the fork inherits whatever CUDA driver state the classifier's torch activity left behind, and a factory reboot
|
| 7 |
+
does not clear it. It cannot be a `multiprocessing.spawn` child either: spawn re-imports the parent's main
|
| 8 |
+
module, and on a Space that main module is `app.py` — the child would re-run the whole startup, `start()`
|
| 9 |
+
included. So the classifier runs this file as a plain subprocess — a fresh interpreter that never sees `spaces`
|
| 10 |
+
— and answers over stdin/stdout, one JSON object per line.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import select
|
| 18 |
+
import subprocess
|
| 19 |
+
import sys
|
| 20 |
+
import threading
|
| 21 |
+
|
| 22 |
+
GUARD_REPO = "hfmlsoc/ncii-light-guard-v01"
|
| 23 |
+
|
| 24 |
+
_lock = threading.Lock()
|
| 25 |
+
_process: subprocess.Popen | None = None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _read(timeout: float) -> dict:
|
| 29 |
+
readable, _, _ = select.select([_process.stdout], [], [], timeout)
|
| 30 |
+
if not readable:
|
| 31 |
+
raise TimeoutError(f"the guard did not answer within {timeout}s")
|
| 32 |
+
line = _process.stdout.readline()
|
| 33 |
+
if not line:
|
| 34 |
+
raise EOFError("the guard process died")
|
| 35 |
+
return json.loads(line)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _spawn() -> None:
|
| 39 |
+
global _process
|
| 40 |
+
_process = subprocess.Popen(
|
| 41 |
+
[sys.executable, os.path.abspath(__file__)],
|
| 42 |
+
stdin=subprocess.PIPE,
|
| 43 |
+
stdout=subprocess.PIPE,
|
| 44 |
+
text=True,
|
| 45 |
+
bufsize=1,
|
| 46 |
+
)
|
| 47 |
+
# Generous: a cold cache downloads the checkpoint first.
|
| 48 |
+
assert _read(300.0) == {"status": "ready"}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def start() -> None:
|
| 52 |
+
"""Launch the worker and block until its model is up. Called once at startup; `classify` revives it if it dies."""
|
| 53 |
+
with _lock:
|
| 54 |
+
_spawn()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def classify(prompt: str, timeout: float = 60.0) -> dict:
|
| 58 |
+
"""`{'label': 'safe' | 'ncii', 'score': ...}` for one prompt, replacing a dead or wedged worker once."""
|
| 59 |
+
with _lock:
|
| 60 |
+
for attempt in (0, 1):
|
| 61 |
+
try:
|
| 62 |
+
if _process is None or _process.poll() is not None:
|
| 63 |
+
_spawn()
|
| 64 |
+
_process.stdin.write(json.dumps({"prompt": prompt}) + "\n")
|
| 65 |
+
_process.stdin.flush()
|
| 66 |
+
return _read(timeout)
|
| 67 |
+
except Exception:
|
| 68 |
+
if attempt:
|
| 69 |
+
raise
|
| 70 |
+
if _process is not None and _process.poll() is None:
|
| 71 |
+
_process.kill()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _serve() -> None:
|
| 75 |
+
"""The child: plain torch on CPU. The protocol keeps the real stdout to itself — everything else
|
| 76 |
+
(download progress, warnings) is pushed over to stderr so it cannot corrupt a reply."""
|
| 77 |
+
protocol = os.fdopen(os.dup(1), "w", buffering=1)
|
| 78 |
+
os.dup2(2, 1)
|
| 79 |
+
|
| 80 |
+
from transformers import pipeline
|
| 81 |
+
|
| 82 |
+
classifier = pipeline("text-classification", model=GUARD_REPO, device="cpu")
|
| 83 |
+
protocol.write(json.dumps({"status": "ready"}) + "\n")
|
| 84 |
+
for line in sys.stdin:
|
| 85 |
+
result = classifier(json.loads(line)["prompt"], truncation=True)[0]
|
| 86 |
+
protocol.write(json.dumps({"label": result["label"], "score": float(result["score"])}) + "\n")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
if __name__ == "__main__":
|
| 90 |
+
_serve()
|