"""The NCII prompt guard, in its own process. [`hfmlsoc/ncii-light-guard-v01`](https://huggingface.co/hfmlsoc/ncii-light-guard-v01) is a 270M CPU text classifier scoring the NCII risk of an edit prompt. It cannot live in the main process: with it loaded there, every subsequent `@spaces.GPU` worker dies at `worker_init` with `RuntimeError: No CUDA GPUs are available` — the fork inherits whatever CUDA driver state the classifier's torch activity left behind, and a factory reboot does not clear it. It cannot be a `multiprocessing.spawn` child either: spawn re-imports the parent's main module, and on a Space that main module is `app.py` — the child would re-run the whole startup, `start()` included. So the classifier runs this file as a plain subprocess — a fresh interpreter that never sees `spaces` — and answers over stdin/stdout, one JSON object per line. """ from __future__ import annotations import json import os import select import subprocess import sys import threading GUARD_REPO = "hfmlsoc/ncii-light-guard-v01" _lock = threading.Lock() _process: subprocess.Popen | None = None def _read(timeout: float) -> dict: readable, _, _ = select.select([_process.stdout], [], [], timeout) if not readable: raise TimeoutError(f"the guard did not answer within {timeout}s") line = _process.stdout.readline() if not line: raise EOFError("the guard process died") return json.loads(line) def _spawn() -> None: global _process _process = subprocess.Popen( [sys.executable, os.path.abspath(__file__)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1, ) # Generous: a cold cache downloads the checkpoint first. assert _read(300.0) == {"status": "ready"} def start() -> None: """Launch the worker and block until its model is up. Called once at startup; `classify` revives it if it dies.""" with _lock: _spawn() def classify(prompt: str, timeout: float = 60.0) -> dict: """`{'label': 'safe' | 'ncii', 'score': ...}` for one prompt, replacing a dead or wedged worker once.""" with _lock: for attempt in (0, 1): try: if _process is None or _process.poll() is not None: _spawn() _process.stdin.write(json.dumps({"prompt": prompt}) + "\n") _process.stdin.flush() return _read(timeout) except Exception: if attempt: raise if _process is not None and _process.poll() is None: _process.kill() def _serve() -> None: """The child: plain torch on CPU. The protocol keeps the real stdout to itself — everything else (download progress, warnings) is pushed over to stderr so it cannot corrupt a reply.""" protocol = os.fdopen(os.dup(1), "w", buffering=1) os.dup2(2, 1) from transformers import pipeline classifier = pipeline("text-classification", model=GUARD_REPO, device="cpu") protocol.write(json.dumps({"status": "ready"}) + "\n") for line in sys.stdin: result = classifier(json.loads(line)["prompt"], truncation=True)[0] protocol.write(json.dumps({"label": result["label"], "score": float(result["score"])}) + "\n") if __name__ == "__main__": _serve()