Spaces:
Running on Zero
Running on Zero
update bug fix -- @cpu
Browse files
app.py
CHANGED
|
@@ -11,7 +11,7 @@ import base64
|
|
| 11 |
import json
|
| 12 |
from io import BytesIO
|
| 13 |
from PIL import Image
|
| 14 |
-
|
| 15 |
|
| 16 |
MAX_SEED = np.iinfo(np.int32).max
|
| 17 |
LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
|
|
@@ -52,37 +52,35 @@ try:
|
|
| 52 |
except Exception as e:
|
| 53 |
print(f"Warning: Could not set FA3 processor: {e}")
|
| 54 |
|
| 55 |
-
# ββ NCII safety guard (CPU
|
| 56 |
-
NCII_MODEL_ID = "hfmlsoc/ncii-guard-v02"
|
| 57 |
NCII_UNSAFE_LABEL = "ncii"
|
| 58 |
NCII_THRESHOLD = 0.5
|
| 59 |
NCII_BLOCK_MESSAGE = "You entered prompt is NCII (non-consensual intimate imagery) and your request will not be processed. Try with Safe Prompts."
|
| 60 |
|
| 61 |
-
print("
|
| 62 |
try:
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
print("NCII guard loaded successfully on CPU.")
|
| 66 |
except Exception as e:
|
| 67 |
-
|
| 68 |
-
print(f"Warning: Could not load NCII guard model ({NCII_MODEL_ID}): {e}")
|
| 69 |
|
| 70 |
|
| 71 |
def check_ncii_safety(prompt_text):
|
| 72 |
-
"""
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
| 74 |
return False, "unknown", 0.0
|
| 75 |
try:
|
| 76 |
-
out = ncii_guard(prompt_text.strip())
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
is_unsafe = (label == NCII_UNSAFE_LABEL) and (score >= NCII_THRESHOLD)
|
| 82 |
-
return is_unsafe, label, score
|
| 83 |
except Exception as e:
|
| 84 |
print(f"NCII guard inference error: {e}")
|
| 85 |
-
|
| 86 |
|
| 87 |
|
| 88 |
# ββ Examples Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -95,7 +93,6 @@ EXAMPLES_CONFIG = [
|
|
| 95 |
{"images": ["examples/10.jpg", "examples/11.png"], "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result."},
|
| 96 |
]
|
| 97 |
|
| 98 |
-
|
| 99 |
def make_thumb_b64(path, max_dim=220):
|
| 100 |
if not os.path.exists(path):
|
| 101 |
return ""
|
|
@@ -109,7 +106,6 @@ def make_thumb_b64(path, max_dim=220):
|
|
| 109 |
print(f"Thumbnail error for {path}: {e}")
|
| 110 |
return ""
|
| 111 |
|
| 112 |
-
|
| 113 |
def encode_full_image(path):
|
| 114 |
if not os.path.exists(path):
|
| 115 |
return ""
|
|
@@ -123,7 +119,6 @@ def encode_full_image(path):
|
|
| 123 |
print(f"Encode error for {path}: {e}")
|
| 124 |
return ""
|
| 125 |
|
| 126 |
-
|
| 127 |
def build_client_config():
|
| 128 |
"""Static config consumed by the frontend: example cards."""
|
| 129 |
examples = []
|
|
@@ -138,12 +133,10 @@ def build_client_config():
|
|
| 138 |
"examples": examples,
|
| 139 |
}
|
| 140 |
|
| 141 |
-
|
| 142 |
print("Building client config (example thumbnails)β¦")
|
| 143 |
CLIENT_CONFIG = build_client_config()
|
| 144 |
print(f"Built config with {len(EXAMPLES_CONFIG)} examples.")
|
| 145 |
|
| 146 |
-
|
| 147 |
def b64_to_pil_list(b64_json_str):
|
| 148 |
if not b64_json_str or b64_json_str.strip() in ("", "[]"):
|
| 149 |
return []
|
|
@@ -166,13 +159,11 @@ def b64_to_pil_list(b64_json_str):
|
|
| 166 |
print(f"Error decoding image: {e}")
|
| 167 |
return pil_images
|
| 168 |
|
| 169 |
-
|
| 170 |
def pil_to_b64_png(image: Image.Image) -> str:
|
| 171 |
buf = BytesIO()
|
| 172 |
image.save(buf, format="PNG")
|
| 173 |
return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
|
| 174 |
|
| 175 |
-
|
| 176 |
def update_dimensions_on_upload(image):
|
| 177 |
if image is None:
|
| 178 |
return 1024, 1024
|
|
@@ -190,31 +181,22 @@ def update_dimensions_on_upload(image):
|
|
| 190 |
app = Server(title="FireRed-Image-Edit-1.0-Fast")
|
| 191 |
|
| 192 |
|
| 193 |
-
# ββ CPU-only safety check endpoint βββββββββββββββββββββββββββββββββββββββββββ
|
| 194 |
-
# This endpoint is NOT decorated with @spaces.GPU, so it runs entirely on CPU
|
| 195 |
-
# and never triggers GPU allocation. The frontend calls this BEFORE submitting
|
| 196 |
-
# the GPU job, so blocked prompts are caught early without wasting GPU resources.
|
| 197 |
@app.api(name="check_safety", queue=False)
|
| 198 |
def check_safety(prompt: str) -> dict:
|
| 199 |
-
"""
|
| 200 |
|
| 201 |
-
Returns
|
|
|
|
|
|
|
| 202 |
"""
|
| 203 |
if not prompt or not prompt.strip():
|
| 204 |
-
return {"
|
| 205 |
-
|
| 206 |
is_unsafe, label, score = check_ncii_safety(prompt)
|
| 207 |
if is_unsafe:
|
| 208 |
-
return {
|
| 209 |
-
|
| 210 |
-
"label": label,
|
| 211 |
-
"score": score,
|
| 212 |
-
"message": NCII_BLOCK_MESSAGE,
|
| 213 |
-
}
|
| 214 |
-
return {"safe": True, "label": label, "score": score, "message": ""}
|
| 215 |
|
| 216 |
|
| 217 |
-
# ββ GPU image-edit endpoint ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 218 |
@app.mcp.tool(name="edit_image")
|
| 219 |
@app.api(name="edit_image")
|
| 220 |
@spaces.GPU(size="xlarge")
|
|
@@ -228,11 +210,8 @@ def infer(
|
|
| 228 |
) -> dict:
|
| 229 |
"""Edit one or more images with FireRed-Image-Edit-1.1.
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
the prompt has already been validated and focuses solely on GPU inference.
|
| 234 |
-
|
| 235 |
-
Returns {"image": <base64 PNG data URL>, "seed": <seed used>, "status": "success"}.
|
| 236 |
"""
|
| 237 |
gc.collect()
|
| 238 |
torch.cuda.empty_cache()
|
|
@@ -243,6 +222,13 @@ def infer(
|
|
| 243 |
if not prompt or prompt.strip() == "":
|
| 244 |
raise gr.Error("Please enter an edit prompt.")
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
if randomize_seed:
|
| 247 |
seed = random.randint(0, MAX_SEED)
|
| 248 |
|
|
|
|
| 11 |
import json
|
| 12 |
from io import BytesIO
|
| 13 |
from PIL import Image
|
| 14 |
+
import ncii_guard
|
| 15 |
|
| 16 |
MAX_SEED = np.iinfo(np.int32).max
|
| 17 |
LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
|
|
|
|
| 52 |
except Exception as e:
|
| 53 |
print(f"Warning: Could not set FA3 processor: {e}")
|
| 54 |
|
| 55 |
+
# ββ NCII safety guard (pure CPU subprocess, never touches spaces/CUDA) ββββββ
|
|
|
|
| 56 |
NCII_UNSAFE_LABEL = "ncii"
|
| 57 |
NCII_THRESHOLD = 0.5
|
| 58 |
NCII_BLOCK_MESSAGE = "You entered prompt is NCII (non-consensual intimate imagery) and your request will not be processed. Try with Safe Prompts."
|
| 59 |
|
| 60 |
+
print("Starting NCII safety guard subprocess (CPU-only)β¦")
|
| 61 |
try:
|
| 62 |
+
ncii_guard.start()
|
| 63 |
+
print("NCII guard subprocess ready.")
|
|
|
|
| 64 |
except Exception as e:
|
| 65 |
+
print(f"Warning: Could not start NCII guard subprocess: {e}")
|
|
|
|
| 66 |
|
| 67 |
|
| 68 |
def check_ncii_safety(prompt_text):
|
| 69 |
+
"""Ask the CPU-side subprocess. No GPU is requested by this call.
|
| 70 |
+
|
| 71 |
+
Returns: (is_unsafe: bool, label: str, score: float)
|
| 72 |
+
"""
|
| 73 |
+
if not prompt_text or not prompt_text.strip():
|
| 74 |
return False, "unknown", 0.0
|
| 75 |
try:
|
| 76 |
+
out = ncii_guard.classify(prompt_text.strip())
|
| 77 |
+
label = str(out.get("label", "")).lower()
|
| 78 |
+
score = float(out.get("score", 0.0))
|
| 79 |
+
is_unsafe = (label == NCII_UNSAFE_LABEL) and (score >= NCII_THRESHOLD)
|
| 80 |
+
return is_unsafe, label, score
|
|
|
|
|
|
|
| 81 |
except Exception as e:
|
| 82 |
print(f"NCII guard inference error: {e}")
|
| 83 |
+
return False, "unknown", 0.0
|
| 84 |
|
| 85 |
|
| 86 |
# ββ Examples Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 93 |
{"images": ["examples/10.jpg", "examples/11.png"], "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result."},
|
| 94 |
]
|
| 95 |
|
|
|
|
| 96 |
def make_thumb_b64(path, max_dim=220):
|
| 97 |
if not os.path.exists(path):
|
| 98 |
return ""
|
|
|
|
| 106 |
print(f"Thumbnail error for {path}: {e}")
|
| 107 |
return ""
|
| 108 |
|
|
|
|
| 109 |
def encode_full_image(path):
|
| 110 |
if not os.path.exists(path):
|
| 111 |
return ""
|
|
|
|
| 119 |
print(f"Encode error for {path}: {e}")
|
| 120 |
return ""
|
| 121 |
|
|
|
|
| 122 |
def build_client_config():
|
| 123 |
"""Static config consumed by the frontend: example cards."""
|
| 124 |
examples = []
|
|
|
|
| 133 |
"examples": examples,
|
| 134 |
}
|
| 135 |
|
|
|
|
| 136 |
print("Building client config (example thumbnails)β¦")
|
| 137 |
CLIENT_CONFIG = build_client_config()
|
| 138 |
print(f"Built config with {len(EXAMPLES_CONFIG)} examples.")
|
| 139 |
|
|
|
|
| 140 |
def b64_to_pil_list(b64_json_str):
|
| 141 |
if not b64_json_str or b64_json_str.strip() in ("", "[]"):
|
| 142 |
return []
|
|
|
|
| 159 |
print(f"Error decoding image: {e}")
|
| 160 |
return pil_images
|
| 161 |
|
|
|
|
| 162 |
def pil_to_b64_png(image: Image.Image) -> str:
|
| 163 |
buf = BytesIO()
|
| 164 |
image.save(buf, format="PNG")
|
| 165 |
return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
|
| 166 |
|
|
|
|
| 167 |
def update_dimensions_on_upload(image):
|
| 168 |
if image is None:
|
| 169 |
return 1024, 1024
|
|
|
|
| 181 |
app = Server(title="FireRed-Image-Edit-1.0-Fast")
|
| 182 |
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
@app.api(name="check_safety", queue=False)
|
| 185 |
def check_safety(prompt: str) -> dict:
|
| 186 |
+
"""Pure-CPU safety check. Runs BEFORE any @spaces.GPU resource is requested.
|
| 187 |
|
| 188 |
+
Returns:
|
| 189 |
+
{"status": "ok"} β prompt is safe, proceed
|
| 190 |
+
{"status": "blocked", "message": ...} β NCII detected
|
| 191 |
"""
|
| 192 |
if not prompt or not prompt.strip():
|
| 193 |
+
return {"status": "ok"}
|
|
|
|
| 194 |
is_unsafe, label, score = check_ncii_safety(prompt)
|
| 195 |
if is_unsafe:
|
| 196 |
+
return {"status": "blocked", "message": NCII_BLOCK_MESSAGE}
|
| 197 |
+
return {"status": "ok", "label": label, "score": score}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
|
|
|
|
| 200 |
@app.mcp.tool(name="edit_image")
|
| 201 |
@app.api(name="edit_image")
|
| 202 |
@spaces.GPU(size="xlarge")
|
|
|
|
| 210 |
) -> dict:
|
| 211 |
"""Edit one or more images with FireRed-Image-Edit-1.1.
|
| 212 |
|
| 213 |
+
The frontend is expected to have already called /check_safety and
|
| 214 |
+
only submit here when safe. This is a defense-in-depth re-check.
|
|
|
|
|
|
|
|
|
|
| 215 |
"""
|
| 216 |
gc.collect()
|
| 217 |
torch.cuda.empty_cache()
|
|
|
|
| 222 |
if not prompt or prompt.strip() == "":
|
| 223 |
raise gr.Error("Please enter an edit prompt.")
|
| 224 |
|
| 225 |
+
# ββ Defense-in-depth NCII re-check inside the GPU worker ββ
|
| 226 |
+
is_unsafe, _, _ = check_ncii_safety(prompt)
|
| 227 |
+
if is_unsafe:
|
| 228 |
+
gc.collect()
|
| 229 |
+
torch.cuda.empty_cache()
|
| 230 |
+
return {"image": "", "seed": seed, "status": "blocked", "message": NCII_BLOCK_MESSAGE}
|
| 231 |
+
|
| 232 |
if randomize_seed:
|
| 233 |
seed = random.randint(0, MAX_SEED)
|
| 234 |
|