Spaces:
Running on Zero
Running on Zero
ImageStudio Maintainer Claude Opus 4.8 (1M context) commited on
Commit ·
2e9546f
1
Parent(s): f4bbab1
feat(safety): adultify realistic prompts, strengthen minor-block negatives, dedicated Qwen guard VLM
Browse files- _adultify_prompt rewrites girl->woman / 1girl->1woman on models flagged
realistic (samanima, realism-illustrious, the 3 z-image); anime keeps 1girl.
- MINOR_NEG force-prepended in _resolve_negative for realistic models even when
the caller supplies a negative; seed loli/child/shota into anime negatives too.
- Split VLM: Gemma stays the prompt assistant; a dedicated GUARD_MODEL_ID
(default Qwen3.5-4B, 8bit) runs _moderate_image_inner, with graceful fallback
to the main VLM on load failure. eos/pad pinned for no-generation_config Qwen.
- Log peak VRAM in the generate GPU window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
app.py
CHANGED
|
@@ -117,12 +117,57 @@ NOOBXL_PREFIX = (
|
|
| 117 |
"very aesthetic, depth of field, highres"
|
| 118 |
)
|
| 119 |
NOOBXL_NEGATIVE = (
|
|
|
|
| 120 |
"worst quality, normal quality, anatomical nonsense, bad anatomy, "
|
| 121 |
"interlocked fingers, extra fingers, watermark, simple background, transparent, "
|
| 122 |
"low quality, logo, text, signature, face backlighting, backlighting"
|
| 123 |
)
|
| 124 |
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
def _apply_prefix(prompt, prefix):
|
| 127 |
"""Prepend ``prefix`` to ``prompt``, dropping any prefix tag the prompt already
|
| 128 |
contains (case-insensitive, per comma-separated token). Empty prefix is a no-op."""
|
|
@@ -171,6 +216,7 @@ IMAGE_MODELS = {
|
|
| 171 |
"realism-illustrious": dict(
|
| 172 |
label="Realism Illustrious (Stable Yogi)", family="illustrious",
|
| 173 |
checkpoint="checkpoints/il/realismIllustriousBy_v55FP16.safetensors",
|
|
|
|
| 174 |
steps=30, guidance=5.0, height=1152, width=896, negative=True,
|
| 175 |
prefix="masterpiece, best quality, amazing quality, very aesthetic, absurdres, highres, ultra detailed",
|
| 176 |
default_negative=(
|
|
@@ -182,16 +228,19 @@ IMAGE_MODELS = {
|
|
| 182 |
"moody-pro-mix": dict(
|
| 183 |
label=MODEL_ZIMAGE, family="zimageturbo",
|
| 184 |
checkpoint="checkpoints/zimageturbo/moodyProMix_zitV12DPO.safetensors",
|
|
|
|
| 185 |
steps=9, guidance=0.0, height=1024, width=1024, negative=False, prefix="",
|
| 186 |
),
|
| 187 |
"pornmaster-zimage": dict(
|
| 188 |
label="PornMaster Z-Image", family="zimageturbo",
|
| 189 |
checkpoint="checkpoints/zimageturbo/pornmasterZImage_turboV35Bf16.safetensors",
|
|
|
|
| 190 |
steps=9, guidance=0.0, height=1024, width=1024, negative=False, prefix="",
|
| 191 |
),
|
| 192 |
"zimage-nsfw-yogi": dict(
|
| 193 |
label="Z-Image NSFW (Stable Yogi)", family="zimageturbo",
|
| 194 |
checkpoint="checkpoints/zimageturbo/zimageTurboNSFWBy_2602NSFWBF16.safetensors",
|
|
|
|
| 195 |
steps=9, guidance=0.0, height=1024, width=1024, negative=False, prefix="",
|
| 196 |
),
|
| 197 |
"wai-anima": dict(
|
|
@@ -200,12 +249,14 @@ IMAGE_MODELS = {
|
|
| 200 |
steps=24, guidance=0.0, height=1024, width=1024, negative=True,
|
| 201 |
prefix="masterpiece, best quality,score_7,",
|
| 202 |
default_negative=(
|
|
|
|
| 203 |
"worst quality, low quality, score_1, score_2, score_3, artist name,"
|
| 204 |
"blurry, jpeg artifacts, lowres,censor"),
|
| 205 |
),
|
| 206 |
"samanima": dict(
|
| 207 |
label="Realistic", family="anima",
|
| 208 |
checkpoint="checkpoints/anima/samANIMA_v20.safetensors",
|
|
|
|
| 209 |
steps=24, guidance=0.0, height=1024, width=1024, negative=True,
|
| 210 |
prefix="masterpiece, best quality, score_7,",
|
| 211 |
default_negative=(
|
|
@@ -235,10 +286,17 @@ def _model_entry(model_name):
|
|
| 235 |
def _resolve_negative(entry, negative_prompt, use_negative_prompt, model_name):
|
| 236 |
"""The negative prompt to actually use: the caller's (when they enabled it and
|
| 237 |
this model honors negatives), else the model's `default_negative`. Disabled or
|
| 238 |
-
unsupported -> empty.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
if not (use_negative_prompt and _supports_negative(model_name)):
|
| 240 |
return ""
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
|
| 244 |
# Which models to load this boot. Unknown keys are ignored; an empty / all-invalid
|
|
@@ -396,21 +454,16 @@ print(f"Image models loaded: {list(PIPELINES)}")
|
|
| 396 |
VLM_MODEL_ID = os.environ.get("VLM_MODEL_ID", "prithivMLmods/gemma-4-E4B-it-Uncensored-MAX")
|
| 397 |
VLM_LOAD_8BIT = os.environ.get("VLM_LOAD_8BIT", "0").lower() not in ("0", "false", "no", "")
|
| 398 |
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
)
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
_vlm_kwargs["device_map"] = "auto"
|
| 410 |
-
vlm_model = AutoModelForImageTextToText.from_pretrained(VLM_MODEL_ID, **_vlm_kwargs)
|
| 411 |
-
if not VLM_LOAD_8BIT:
|
| 412 |
-
vlm_model.to("cuda")
|
| 413 |
-
vlm_model.eval()
|
| 414 |
|
| 415 |
|
| 416 |
def _resolve_vlm_eos_ids(tokenizer, model):
|
|
@@ -441,14 +494,54 @@ def _resolve_vlm_eos_ids(tokenizer, model):
|
|
| 441 |
return sorted(ids)
|
| 442 |
|
| 443 |
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
)
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
|
| 454 |
# =============================================================================
|
|
@@ -574,7 +667,10 @@ def _parse_moderation_json(text):
|
|
| 574 |
|
| 575 |
|
| 576 |
def _moderate_image_inner(image, max_new_tokens=320):
|
| 577 |
-
"""Synchronously run the VLM moderation check on a PIL image
|
|
|
|
|
|
|
|
|
|
| 578 |
messages = [{
|
| 579 |
"role": "user",
|
| 580 |
"content": [
|
|
@@ -582,16 +678,19 @@ def _moderate_image_inner(image, max_new_tokens=320):
|
|
| 582 |
{"type": "text", "text": MODERATION_PROMPT},
|
| 583 |
],
|
| 584 |
}]
|
| 585 |
-
inputs =
|
| 586 |
messages, tokenize=True, add_generation_prompt=True,
|
| 587 |
return_dict=True, return_tensors="pt", enable_thinking=False,
|
| 588 |
-
).to(
|
| 589 |
with torch.inference_mode():
|
| 590 |
-
generated =
|
| 591 |
**inputs, max_new_tokens=int(max_new_tokens), do_sample=False,
|
|
|
|
|
|
|
|
|
|
| 592 |
)
|
| 593 |
trimmed = generated[0][inputs["input_ids"].shape[-1]:]
|
| 594 |
-
tokenizer = getattr(
|
| 595 |
text = tokenizer.decode(trimmed, skip_special_tokens=True)
|
| 596 |
return _parse_moderation_json(text)
|
| 597 |
|
|
@@ -859,6 +958,14 @@ def generate_image(
|
|
| 859 |
if improved:
|
| 860 |
effective_prompt = improved
|
| 861 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
q = queue.Queue()
|
| 863 |
result = {}
|
| 864 |
|
|
@@ -904,6 +1011,16 @@ def generate_image(
|
|
| 904 |
except Exception as exc: # noqa: BLE001 - never let moderation break generation
|
| 905 |
moderation = {"ok": False, "rating": None, "confidence": None,
|
| 906 |
"flags": None, "raw": "", "error": f"{type(exc).__name__}: {exc}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
yield ("image", image, result["seed"], moderation, effective_prompt)
|
| 908 |
|
| 909 |
|
|
|
|
| 117 |
"very aesthetic, depth of field, highres"
|
| 118 |
)
|
| 119 |
NOOBXL_NEGATIVE = (
|
| 120 |
+
"loli, child, shota, toddler, "
|
| 121 |
"worst quality, normal quality, anatomical nonsense, bad anatomy, "
|
| 122 |
"interlocked fingers, extra fingers, watermark, simple background, transparent, "
|
| 123 |
"low quality, logo, text, signature, face backlighting, backlighting"
|
| 124 |
)
|
| 125 |
|
| 126 |
|
| 127 |
+
# ---------------------------------------------------------------------------
|
| 128 |
+
# Minor-safety guards (gated on a model's ``realistic`` registry flag).
|
| 129 |
+
#
|
| 130 |
+
# Photoreal checkpoints inherit a youthful "anime girl" prior from booru tags
|
| 131 |
+
# like ``1girl`` (age-neutral in tag-space, but young-skewing in practice). On a
|
| 132 |
+
# stylized anime model that reads as harmless convention; rendered photoreal the
|
| 133 |
+
# same prior can look like a real minor. Two defenses, both realistic-only so the
|
| 134 |
+
# anime models keep their intended ``1girl`` behaviour:
|
| 135 |
+
# 1. Rewrite youthful subject tokens in the POSITIVE prompt to adult ones
|
| 136 |
+
# (``girl`` -> ``woman``, ``1girl`` -> ``1woman``) via _adultify_prompt.
|
| 137 |
+
# 2. Force a strong minor-blocking NEGATIVE (MINOR_NEG) that is applied even
|
| 138 |
+
# when the caller supplies their own negative (see _resolve_negative).
|
| 139 |
+
MINOR_NEG = (
|
| 140 |
+
"child, children, kid, toddler, infant, baby, loli, shota, preteen, teen, "
|
| 141 |
+
"teenager, adolescent, underage, minor, young girl, little girl, schoolgirl, "
|
| 142 |
+
"child-like face, childlike body, childish proportions, chibi"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# Female subject counters/tokens: standalone ``girl``/``girls`` and the booru
|
| 146 |
+
# ``Ngirl(s)`` forms. The lookbehind ignores letters/digits already attached so
|
| 147 |
+
# ``girlfriend`` and ``cowgirl`` are left intact; the trailing lookahead keeps it
|
| 148 |
+
# from biting into ``girlish`` etc.
|
| 149 |
+
_GIRL_RE = re.compile(r"(?<![a-z0-9])(\d*)\s*girls?(?![a-z])", re.IGNORECASE)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _adultify_prompt(prompt):
|
| 153 |
+
"""Replace youthful female subject tokens with adult equivalents.
|
| 154 |
+
|
| 155 |
+
``girl`` -> ``woman``, ``girls`` / ``2girls`` -> ``women`` / ``2women``,
|
| 156 |
+
``1girl`` -> ``1woman``. Only the bare subject token is touched; compounds
|
| 157 |
+
like ``girlfriend`` and ``cowgirl`` are preserved by the regex boundaries.
|
| 158 |
+
"""
|
| 159 |
+
if not prompt:
|
| 160 |
+
return prompt
|
| 161 |
+
|
| 162 |
+
def _repl(match):
|
| 163 |
+
digits = match.group(1) or ""
|
| 164 |
+
had_s = match.group(0).rstrip().lower().endswith("s")
|
| 165 |
+
plural = had_s or (digits not in ("", "1"))
|
| 166 |
+
return f"{digits}{'women' if plural else 'woman'}"
|
| 167 |
+
|
| 168 |
+
return _GIRL_RE.sub(_repl, prompt)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
def _apply_prefix(prompt, prefix):
|
| 172 |
"""Prepend ``prefix`` to ``prompt``, dropping any prefix tag the prompt already
|
| 173 |
contains (case-insensitive, per comma-separated token). Empty prefix is a no-op."""
|
|
|
|
| 216 |
"realism-illustrious": dict(
|
| 217 |
label="Realism Illustrious (Stable Yogi)", family="illustrious",
|
| 218 |
checkpoint="checkpoints/il/realismIllustriousBy_v55FP16.safetensors",
|
| 219 |
+
realistic=True,
|
| 220 |
steps=30, guidance=5.0, height=1152, width=896, negative=True,
|
| 221 |
prefix="masterpiece, best quality, amazing quality, very aesthetic, absurdres, highres, ultra detailed",
|
| 222 |
default_negative=(
|
|
|
|
| 228 |
"moody-pro-mix": dict(
|
| 229 |
label=MODEL_ZIMAGE, family="zimageturbo",
|
| 230 |
checkpoint="checkpoints/zimageturbo/moodyProMix_zitV12DPO.safetensors",
|
| 231 |
+
realistic=True,
|
| 232 |
steps=9, guidance=0.0, height=1024, width=1024, negative=False, prefix="",
|
| 233 |
),
|
| 234 |
"pornmaster-zimage": dict(
|
| 235 |
label="PornMaster Z-Image", family="zimageturbo",
|
| 236 |
checkpoint="checkpoints/zimageturbo/pornmasterZImage_turboV35Bf16.safetensors",
|
| 237 |
+
realistic=True,
|
| 238 |
steps=9, guidance=0.0, height=1024, width=1024, negative=False, prefix="",
|
| 239 |
),
|
| 240 |
"zimage-nsfw-yogi": dict(
|
| 241 |
label="Z-Image NSFW (Stable Yogi)", family="zimageturbo",
|
| 242 |
checkpoint="checkpoints/zimageturbo/zimageTurboNSFWBy_2602NSFWBF16.safetensors",
|
| 243 |
+
realistic=True,
|
| 244 |
steps=9, guidance=0.0, height=1024, width=1024, negative=False, prefix="",
|
| 245 |
),
|
| 246 |
"wai-anima": dict(
|
|
|
|
| 249 |
steps=24, guidance=0.0, height=1024, width=1024, negative=True,
|
| 250 |
prefix="masterpiece, best quality,score_7,",
|
| 251 |
default_negative=(
|
| 252 |
+
"loli, child, shota, toddler, "
|
| 253 |
"worst quality, low quality, score_1, score_2, score_3, artist name,"
|
| 254 |
"blurry, jpeg artifacts, lowres,censor"),
|
| 255 |
),
|
| 256 |
"samanima": dict(
|
| 257 |
label="Realistic", family="anima",
|
| 258 |
checkpoint="checkpoints/anima/samANIMA_v20.safetensors",
|
| 259 |
+
realistic=True,
|
| 260 |
steps=24, guidance=0.0, height=1024, width=1024, negative=True,
|
| 261 |
prefix="masterpiece, best quality, score_7,",
|
| 262 |
default_negative=(
|
|
|
|
| 286 |
def _resolve_negative(entry, negative_prompt, use_negative_prompt, model_name):
|
| 287 |
"""The negative prompt to actually use: the caller's (when they enabled it and
|
| 288 |
this model honors negatives), else the model's `default_negative`. Disabled or
|
| 289 |
+
unsupported -> empty.
|
| 290 |
+
|
| 291 |
+
Realistic-leaned models additionally PREPEND the minor-safety block
|
| 292 |
+
(``MINOR_NEG``) unconditionally — even when the caller passes their own
|
| 293 |
+
negative, which would otherwise replace the default and drop the guard."""
|
| 294 |
if not (use_negative_prompt and _supports_negative(model_name)):
|
| 295 |
return ""
|
| 296 |
+
neg = (negative_prompt or "").strip() or entry.get("default_negative", "")
|
| 297 |
+
if entry.get("realistic"):
|
| 298 |
+
neg = f"{MINOR_NEG}, {neg}" if neg else MINOR_NEG
|
| 299 |
+
return neg
|
| 300 |
|
| 301 |
|
| 302 |
# Which models to load this boot. Unknown keys are ignored; an empty / all-invalid
|
|
|
|
| 454 |
VLM_MODEL_ID = os.environ.get("VLM_MODEL_ID", "prithivMLmods/gemma-4-E4B-it-Uncensored-MAX")
|
| 455 |
VLM_LOAD_8BIT = os.environ.get("VLM_LOAD_8BIT", "0").lower() not in ("0", "false", "no", "")
|
| 456 |
|
| 457 |
+
# Dedicated image-moderation guard VLM, kept SEPARATE from the prompt-assistant
|
| 458 |
+
# VLM so each can be specialized: Gemma (above) writes/improves prompts — fluent
|
| 459 |
+
# and fast — while a stricter rater judges generated images. Defaults to a
|
| 460 |
+
# Qwen3.5-4B in 8-bit (~4.5 GB) so it co-resides with Gemma + the diffusion
|
| 461 |
+
# pipelines on the zero-a10g. The guard MUST be vision-capable
|
| 462 |
+
# (AutoModelForImageTextToText) — confirm any override is a Qwen3.5-VL / other
|
| 463 |
+
# multimodal checkpoint. Set GUARD_MODEL_ID="" to disable the split and moderate
|
| 464 |
+
# with the main VLM, and GUARD_LOAD_8BIT=0 to load it in bf16.
|
| 465 |
+
GUARD_MODEL_ID = os.environ.get("GUARD_MODEL_ID", "rodrigomt/Qwen3.5-4B-Uncensored-Aggressive")
|
| 466 |
+
GUARD_LOAD_8BIT = os.environ.get("GUARD_LOAD_8BIT", "1").lower() not in ("0", "false", "no", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
|
| 469 |
def _resolve_vlm_eos_ids(tokenizer, model):
|
|
|
|
| 494 |
return sorted(ids)
|
| 495 |
|
| 496 |
|
| 497 |
+
def _load_vlm(model_id, load_8bit, role="VLM"):
|
| 498 |
+
"""Load a multimodal VLM (processor + model) and resolve its stop/pad ids.
|
| 499 |
+
|
| 500 |
+
bf16 by default; ``load_8bit`` uses bitsandbytes — which quantizes on GPU as
|
| 501 |
+
the weights load via ``device_map='auto'``, so we must NOT call ``.to('cuda')``
|
| 502 |
+
afterwards (it errors on a quantized model). Returns
|
| 503 |
+
``(processor, model, eos_ids, pad_id)``."""
|
| 504 |
+
print(f"Loading {role}: {model_id} (8bit={load_8bit}) ...")
|
| 505 |
+
processor = AutoProcessor.from_pretrained(model_id, token=HF_TOKEN, trust_remote_code=True)
|
| 506 |
+
kwargs = dict(token=HF_TOKEN, trust_remote_code=True, torch_dtype=torch.bfloat16)
|
| 507 |
+
if load_8bit:
|
| 508 |
+
kwargs["quantization_config"] = BitsAndBytesConfig(load_in_8bit=True)
|
| 509 |
+
kwargs["device_map"] = "auto"
|
| 510 |
+
model = AutoModelForImageTextToText.from_pretrained(model_id, **kwargs)
|
| 511 |
+
if not load_8bit:
|
| 512 |
+
model.to("cuda")
|
| 513 |
+
model.eval()
|
| 514 |
+
tokenizer = getattr(processor, "tokenizer", processor)
|
| 515 |
+
eos_ids = _resolve_vlm_eos_ids(tokenizer, model)
|
| 516 |
+
pad_id = (tokenizer.pad_token_id if tokenizer.pad_token_id is not None
|
| 517 |
+
else (eos_ids[0] if eos_ids else None))
|
| 518 |
+
print(f"{role} loaded! (eos_ids={eos_ids}, pad_id={pad_id})")
|
| 519 |
+
return processor, model, eos_ids, pad_id
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
# Prompt-assistant VLM (Gemma): the "Prompt Assistant" tab + the prompt-improve
|
| 523 |
+
# pass. One-turn Q&A with an optional image; turns rough ideas into rich prompts.
|
| 524 |
+
vlm_processor, vlm_model, _VLM_EOS_IDS, _VLM_PAD_ID = _load_vlm(
|
| 525 |
+
VLM_MODEL_ID, VLM_LOAD_8BIT, role="assistant VLM"
|
| 526 |
)
|
| 527 |
+
|
| 528 |
+
# Image-safety guard VLM (moderation only). Falls back to the main VLM when
|
| 529 |
+
# disabled (GUARD_MODEL_ID="" or == the assistant) or if loading fails (e.g. OOM)
|
| 530 |
+
# so moderation never silently breaks and the Space always boots.
|
| 531 |
+
if GUARD_MODEL_ID and GUARD_MODEL_ID != VLM_MODEL_ID:
|
| 532 |
+
try:
|
| 533 |
+
guard_processor, guard_model, _GUARD_EOS_IDS, _GUARD_PAD_ID = _load_vlm(
|
| 534 |
+
GUARD_MODEL_ID, GUARD_LOAD_8BIT, role="guard VLM"
|
| 535 |
+
)
|
| 536 |
+
except Exception as exc: # noqa: BLE001 - degrade gracefully, never block boot
|
| 537 |
+
print(f"[guard] failed to load {GUARD_MODEL_ID}: {type(exc).__name__}: {exc}; "
|
| 538 |
+
f"moderation will use the main VLM ({VLM_MODEL_ID})", flush=True)
|
| 539 |
+
guard_processor, guard_model = vlm_processor, vlm_model
|
| 540 |
+
_GUARD_EOS_IDS, _GUARD_PAD_ID = _VLM_EOS_IDS, _VLM_PAD_ID
|
| 541 |
+
else:
|
| 542 |
+
guard_processor, guard_model = vlm_processor, vlm_model
|
| 543 |
+
_GUARD_EOS_IDS, _GUARD_PAD_ID = _VLM_EOS_IDS, _VLM_PAD_ID
|
| 544 |
+
print(f"Guard VLM: using main VLM ({VLM_MODEL_ID}) for moderation")
|
| 545 |
|
| 546 |
|
| 547 |
# =============================================================================
|
|
|
|
| 667 |
|
| 668 |
|
| 669 |
def _moderate_image_inner(image, max_new_tokens=320):
|
| 670 |
+
"""Synchronously run the safety-guard VLM moderation check on a PIL image
|
| 671 |
+
(GPU-side). Uses the dedicated guard model (``guard_model``) — which may be a
|
| 672 |
+
different, stricter VLM than the prompt assistant — with greedy decoding and
|
| 673 |
+
NO repetition penalties (those mangle the repetitive rating JSON)."""
|
| 674 |
messages = [{
|
| 675 |
"role": "user",
|
| 676 |
"content": [
|
|
|
|
| 678 |
{"type": "text", "text": MODERATION_PROMPT},
|
| 679 |
],
|
| 680 |
}]
|
| 681 |
+
inputs = guard_processor.apply_chat_template(
|
| 682 |
messages, tokenize=True, add_generation_prompt=True,
|
| 683 |
return_dict=True, return_tensors="pt", enable_thinking=False,
|
| 684 |
+
).to(guard_model.device)
|
| 685 |
with torch.inference_mode():
|
| 686 |
+
generated = guard_model.generate(
|
| 687 |
**inputs, max_new_tokens=int(max_new_tokens), do_sample=False,
|
| 688 |
+
# Pin stop/pad ids so a guard model without a generation_config (e.g.
|
| 689 |
+
# the Qwen3.5 finetunes) terminates instead of rambling to the budget.
|
| 690 |
+
eos_token_id=_GUARD_EOS_IDS, pad_token_id=_GUARD_PAD_ID,
|
| 691 |
)
|
| 692 |
trimmed = generated[0][inputs["input_ids"].shape[-1]:]
|
| 693 |
+
tokenizer = getattr(guard_processor, "tokenizer", guard_processor)
|
| 694 |
text = tokenizer.decode(trimmed, skip_special_tokens=True)
|
| 695 |
return _parse_moderation_json(text)
|
| 696 |
|
|
|
|
| 958 |
if improved:
|
| 959 |
effective_prompt = improved
|
| 960 |
|
| 961 |
+
# Minor-safety: on realistic-leaned models, rewrite youthful subject tokens to
|
| 962 |
+
# adult ones (e.g. the photoreal `1girl` -> `1woman`). Applied after any
|
| 963 |
+
# prompt-improve pass so the rewrite can't be reintroduced, and echoed back as
|
| 964 |
+
# `effective_prompt`. Anime models are not flagged `realistic`, so they keep
|
| 965 |
+
# `1girl` unchanged.
|
| 966 |
+
if _model_entry(model_name).get("realistic"):
|
| 967 |
+
effective_prompt = _adultify_prompt(effective_prompt)
|
| 968 |
+
|
| 969 |
q = queue.Queue()
|
| 970 |
result = {}
|
| 971 |
|
|
|
|
| 1011 |
except Exception as exc: # noqa: BLE001 - never let moderation break generation
|
| 1012 |
moderation = {"ok": False, "rating": None, "confidence": None,
|
| 1013 |
"flags": None, "raw": "", "error": f"{type(exc).__name__}: {exc}"}
|
| 1014 |
+
# VRAM report: peak usage in this GPU window, with the diffusion pipeline +
|
| 1015 |
+
# both VLMs (assistant + guard) resident. Lets us see what the guard split
|
| 1016 |
+
# actually costs on the zero-a10g.
|
| 1017 |
+
if torch.cuda.is_available():
|
| 1018 |
+
print(
|
| 1019 |
+
f"[ImageStudio] VRAM peak={torch.cuda.max_memory_allocated()/2**30:.2f} GB "
|
| 1020 |
+
f"reserved={torch.cuda.memory_reserved()/2**30:.2f} GB "
|
| 1021 |
+
f"(guard={GUARD_MODEL_ID or 'main-vlm'} 8bit={GUARD_LOAD_8BIT})",
|
| 1022 |
+
flush=True,
|
| 1023 |
+
)
|
| 1024 |
yield ("image", image, result["seed"], moderation, effective_prompt)
|
| 1025 |
|
| 1026 |
|