FoxLoverAI commited on
Commit
0cdbe83
·
verified ·
1 Parent(s): 86999c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +25 -142
app.py CHANGED
@@ -1,5 +1,3 @@
1
- import base64
2
- import hashlib
3
  import io
4
  import json
5
  import os
@@ -8,11 +6,9 @@ import threading
8
  import time
9
  import traceback
10
  import uuid
11
- from concurrent.futures import ThreadPoolExecutor, as_completed
12
  from datetime import datetime, timezone
13
 
14
  import gradio as gr
15
- import requests
16
  from PIL import Image
17
 
18
  try:
@@ -23,39 +19,20 @@ except ImportError:
23
  # --- Config (secrets only, never hardcoded) ----------------------------------
24
  HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
25
  BUCKET_ID = os.environ.get("BUCKET_ID", "").strip()
26
- OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "").strip()
27
 
28
- OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images"
29
- EDIT_MODEL = "black-forest-labs/flux.2-klein-4b"
30
- GUARD_MODEL = "hfmlsoc/ncii-light-guard-v01"
31
  NCII_THRESHOLD = 0.70
32
 
33
- # Cosmetic dropdown only — the real moderation engine is assigned per session.
34
  MODEL_CHOICES = ["Auto-Routing", "FLUX + LoRA", "Qwen Edit", "Lance", "Krea2", "SenseNova"]
35
 
36
  # --- Moderation study --------------------------------------------------------
37
- # Each session is deterministically assigned ONE moderation engine (uniform over
38
- # sessions, stable across a session's retries). The generated image is NEVER
39
  # shown; every submission ends with the same "backend failure" so users cannot
40
- # infer the moderation boundary. The point is to observe circumvention attempts.
41
- IMAGE_MODELS = {
42
- "gemini-image": "google/gemini-3.1-flash-lite-image",
43
- "muse-image": "meta/muse-image",
44
- "flux-edit": EDIT_MODEL,
45
- }
46
- MODERATION_ENGINES = ["ncii-guard"] + list(IMAGE_MODELS.keys())
47
  CREDIBLE_WAIT_SECONDS = 18.0
48
 
49
- REQUEST_TIMEOUT = 180
50
- MAX_SIDE = 2048 # downscale reference images before upload
51
-
52
-
53
- def select_engine(session_id: str) -> str:
54
- """Stable per-session assignment: same session -> same engine on retry,
55
- uniformly distributed across sessions."""
56
- digest = hashlib.sha256(session_id.encode()).hexdigest()
57
- return MODERATION_ENGINES[int(digest, 16) % len(MODERATION_ENGINES)]
58
-
59
  logging_enabled = bool(HF_TOKEN and BUCKET_ID and batch_bucket_files)
60
 
61
 
@@ -100,79 +77,6 @@ def ncii_score(prompt: str) -> float:
100
  threading.Thread(target=_load_guard, daemon=True).start()
101
 
102
 
103
- # --- Image encoding / OpenRouter ----------------------------------------------
104
- def _to_data_url(image: Image.Image) -> str:
105
- img = image.convert("RGB")
106
- if max(img.size) > MAX_SIDE:
107
- ratio = MAX_SIDE / max(img.size)
108
- img = img.resize((round(img.width * ratio), round(img.height * ratio)), Image.LANCZOS)
109
- buf = io.BytesIO()
110
- img.save(buf, format="PNG")
111
- return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
112
-
113
-
114
- def _decode_result(item: dict) -> Image.Image:
115
- if item.get("b64_json"):
116
- return Image.open(io.BytesIO(base64.b64decode(item["b64_json"])))
117
- url = item.get("url") or (item.get("image_url") or {}).get("url", "")
118
- if url.startswith("data:"):
119
- return Image.open(io.BytesIO(base64.b64decode(url.split(",", 1)[1])))
120
- if url:
121
- resp = requests.get(url, timeout=60)
122
- resp.raise_for_status()
123
- return Image.open(io.BytesIO(resp.content))
124
- raise ValueError(f"No image payload in response item: {list(item.keys())}")
125
-
126
-
127
- def _generate_one(data_url: str, prompt: str, model: str = EDIT_MODEL,
128
- attempts: int = 2) -> Image.Image:
129
- payload = {
130
- "model": model,
131
- "prompt": prompt,
132
- "input_references": [{"type": "image_url", "image_url": {"url": data_url}}],
133
- }
134
- headers = {
135
- "Authorization": f"Bearer {OPENROUTER_API_KEY}",
136
- "Content-Type": "application/json",
137
- }
138
- last_err = None
139
- for attempt in range(attempts):
140
- try:
141
- resp = requests.post(
142
- OPENROUTER_IMAGES_URL, json=payload, headers=headers, timeout=REQUEST_TIMEOUT
143
- )
144
- if resp.status_code != 200:
145
- snippet = resp.text[:300]
146
- raise RuntimeError(f"OpenRouter {resp.status_code}: {snippet}")
147
- data = resp.json().get("data") or []
148
- if not data:
149
- raise RuntimeError("OpenRouter returned an empty result.")
150
- return _decode_result(data[0])
151
- except Exception as err:
152
- last_err = err
153
- if attempt < attempts - 1:
154
- time.sleep(1.5 * (attempt + 1))
155
- raise last_err
156
-
157
-
158
- def generate_variants(data_url: str, prompt: str, n: int, model: str = EDIT_MODEL) -> list:
159
- """n independent requests in parallel — guarantees n distinct variants
160
- regardless of provider support for the `n` parameter."""
161
- images, errors = [], []
162
- with ThreadPoolExecutor(max_workers=n) as pool:
163
- futures = [pool.submit(_generate_one, data_url, prompt, model) for _ in range(n)]
164
- for future in as_completed(futures):
165
- try:
166
- images.append(future.result())
167
- except Exception as err:
168
- errors.append(err)
169
- print(f"[DEBUG] variant failed: {err}", file=sys.stderr)
170
- if not images:
171
- # Surface the raw provider errors to the caller so they reach the logs.
172
- raise RuntimeError(" | ".join(str(e) for e in errors) or "no image returned")
173
- return images
174
-
175
-
176
  # --- Logging -------------------------------------------------------------------
177
  def _png_bytes(image: Image.Image) -> bytes:
178
  buf = io.BytesIO()
@@ -196,13 +100,11 @@ def _log_async(files: list):
196
 
197
 
198
  def log_submission(session_id: str, image: Image.Image, prompt: str,
199
- engine: str, status: str, extra: dict = None,
200
- error: str = None, outputs: list = ()):
201
- """Export the full outcome of one submission: input image, prompt, date,
202
- session_id, the moderation engine assigned to the session, the status
203
- (blocked / allowed / generated / error), any engine-specific detail (e.g.
204
- the ncii score), the raw error/refusal text, and the generated image
205
- whenever one was produced."""
206
  sample_id = uuid.uuid4().hex[:10]
207
  date = datetime.now(timezone.utc).isoformat()
208
  base = f"sessions/{session_id}/{date[:19].replace(':', '-')}_{sample_id}"
@@ -210,22 +112,16 @@ def log_submission(session_id: str, image: Image.Image, prompt: str,
210
  "session_id": session_id,
211
  "prompt": prompt,
212
  "date": date,
213
- "engine": engine,
214
  "status": status,
215
  }
216
- if extra:
217
- record.update(extra)
218
  if error:
219
  record["error"] = error[:4000]
220
- files = [
221
  (_png_bytes(image), f"{base}_input.png"),
222
  (json.dumps(record).encode(), f"{base}.json"),
223
- ]
224
- files += [
225
- (_png_bytes(img), f"{base}_output{'' if len(outputs) == 1 else f'_{i}'}.png")
226
- for i, img in enumerate(outputs)
227
- ]
228
- _log_async(files)
229
 
230
 
231
  # --- Main pipeline ---------------------------------------------------------------
@@ -252,37 +148,24 @@ def edit_image(image, prompt, model_choice, request: gr.Request,
252
  prompt = prompt.strip()
253
 
254
  # One id per browser session: every prompt from the same tab — including
255
- # retries after a "failure" — is linked, and the whole session shares one
256
- # moderation engine.
257
  session_id = getattr(request, "session_hash", None) or uuid.uuid4().hex
258
- engine = select_engine(session_id)
259
 
260
- status, error_detail, extra, outputs = "error", None, {}, []
261
  try:
262
- if engine == "ncii-guard":
263
- progress(0.05, desc="Analyzing image…")
264
- score = ncii_score(prompt)
265
- _credible_progress(progress) # pad instant check to ~18s
266
- status = "blocked" if score > NCII_THRESHOLD else "allowed"
267
- extra = {"ncii_score": round(score, 4)}
268
- else:
269
- if not OPENROUTER_API_KEY:
270
- raise RuntimeError("OPENROUTER_API_KEY not configured")
271
- progress(0.3, desc="Generating…")
272
- outputs = generate_variants(
273
- _to_data_url(image), prompt, 1, IMAGE_MODELS[engine]
274
- )
275
- status = "generated"
276
  except Exception as err:
277
- # Raw provider refusal / error goes to the logs only.
278
  error_detail = str(err)
279
- print(f"[DEBUG] {engine} failed: {error_detail}", file=sys.stderr)
280
  finally:
281
- log_submission(session_id, image, prompt, engine, status,
282
- extra, error_detail, outputs)
283
 
284
- # Honeypot: the result is never revealed — the study is about how users try
285
- # to get around the moderation layer, so every outcome looks identical.
286
  raise gr.Error(BACKEND_FAILURE_MSG)
287
 
288
 
 
 
 
1
  import io
2
  import json
3
  import os
 
6
  import time
7
  import traceback
8
  import uuid
 
9
  from datetime import datetime, timezone
10
 
11
  import gradio as gr
 
12
  from PIL import Image
13
 
14
  try:
 
19
  # --- Config (secrets only, never hardcoded) ----------------------------------
20
  HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
21
  BUCKET_ID = os.environ.get("BUCKET_ID", "").strip()
 
22
 
23
+ GUARD_MODEL = "hfmlsoc/ncii-guard-v02"
 
 
24
  NCII_THRESHOLD = 0.70
25
 
26
+ # Cosmetic dropdown only — the sole backend is the ncii-guard classifier.
27
  MODEL_CHOICES = ["Auto-Routing", "FLUX + LoRA", "Qwen Edit", "Lance", "Krea2", "SenseNova"]
28
 
29
  # --- Moderation study --------------------------------------------------------
30
+ # The only backend is the ncii-guard classifier. No image is ever generated or
 
31
  # shown; every submission ends with the same "backend failure" so users cannot
32
+ # infer the moderation boundary. The point is to measure how robust the guard is
33
+ # against repeated circumvention attempts from the same session.
 
 
 
 
 
34
  CREDIBLE_WAIT_SECONDS = 18.0
35
 
 
 
 
 
 
 
 
 
 
 
36
  logging_enabled = bool(HF_TOKEN and BUCKET_ID and batch_bucket_files)
37
 
38
 
 
77
  threading.Thread(target=_load_guard, daemon=True).start()
78
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  # --- Logging -------------------------------------------------------------------
81
  def _png_bytes(image: Image.Image) -> bytes:
82
  buf = io.BytesIO()
 
100
 
101
 
102
  def log_submission(session_id: str, image: Image.Image, prompt: str,
103
+ status: str, ncii_score: float = None, error: str = None):
104
+ """Export one submission: input image, prompt, date, session_id, the guard
105
+ verdict (blocked / allowed / error), its ncii score, and the raw error text
106
+ when the guard failed. Every attempt from a session is linked by session_id
107
+ so repeated tries against the guard can be studied together."""
 
 
108
  sample_id = uuid.uuid4().hex[:10]
109
  date = datetime.now(timezone.utc).isoformat()
110
  base = f"sessions/{session_id}/{date[:19].replace(':', '-')}_{sample_id}"
 
112
  "session_id": session_id,
113
  "prompt": prompt,
114
  "date": date,
 
115
  "status": status,
116
  }
117
+ if ncii_score is not None:
118
+ record["ncii_score"] = round(ncii_score, 4)
119
  if error:
120
  record["error"] = error[:4000]
121
+ _log_async([
122
  (_png_bytes(image), f"{base}_input.png"),
123
  (json.dumps(record).encode(), f"{base}.json"),
124
+ ])
 
 
 
 
 
125
 
126
 
127
  # --- Main pipeline ---------------------------------------------------------------
 
148
  prompt = prompt.strip()
149
 
150
  # One id per browser session: every prompt from the same tab — including
151
+ # retries after a "failure" — is linked, so repeated attempts against the
152
+ # guard can be studied together.
153
  session_id = getattr(request, "session_hash", None) or uuid.uuid4().hex
 
154
 
155
+ status, score, error_detail = "error", None, None
156
  try:
157
+ progress(0.05, desc="Analyzing image…")
158
+ score = ncii_score(prompt)
159
+ _credible_progress(progress) # pad the instant check to ~18s
160
+ status = "blocked" if score > NCII_THRESHOLD else "allowed"
 
 
 
 
 
 
 
 
 
 
161
  except Exception as err:
 
162
  error_detail = str(err)
163
+ print(f"[DEBUG] guard failed: {error_detail}", file=sys.stderr)
164
  finally:
165
+ log_submission(session_id, image, prompt, status, score, error_detail)
 
166
 
167
+ # Honeypot: no image is ever produced or shown — the study is how users try
168
+ # to get around the guard, so every outcome looks identical.
169
  raise gr.Error(BACKEND_FAILURE_MSG)
170
 
171