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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +78 -66
app.py CHANGED
@@ -1,4 +1,5 @@
1
  import base64
 
2
  import io
3
  import json
4
  import os
@@ -29,13 +30,32 @@ EDIT_MODEL = "black-forest-labs/flux.2-klein-4b"
29
  GUARD_MODEL = "hfmlsoc/ncii-light-guard-v01"
30
  NCII_THRESHOLD = 0.70
31
 
 
32
  MODEL_CHOICES = ["Auto-Routing", "FLUX + LoRA", "Qwen Edit", "Lance", "Krea2", "SenseNova"]
33
- # Every choice currently routes to FLUX.2 Klein; swap slugs here to wire real backends.
34
- MODEL_ROUTES = {choice: EDIT_MODEL for choice in MODEL_CHOICES}
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  REQUEST_TIMEOUT = 180
37
  MAX_SIDE = 2048 # downscale reference images before upload
38
 
 
 
 
 
 
 
 
39
  logging_enabled = bool(HF_TOKEN and BUCKET_ID and batch_bucket_files)
40
 
41
 
@@ -108,7 +128,7 @@ def _generate_one(data_url: str, prompt: str, model: str = EDIT_MODEL,
108
  attempts: int = 2) -> Image.Image:
109
  payload = {
110
  "model": model,
111
- "prompt": prompt + " (change the face, we should not recognize the original person)",
112
  "input_references": [{"type": "image_url", "image_url": {"url": data_url}}],
113
  }
114
  headers = {
@@ -176,10 +196,13 @@ def _log_async(files: list):
176
 
177
 
178
  def log_submission(session_id: str, image: Image.Image, prompt: str,
179
- status: str, error: str = None, outputs: list = ()):
 
180
  """Export the full outcome of one submission: input image, prompt, date,
181
- session_id, status (blocked / generated / error), the raw error detail
182
- when something failed, and the output image when one was generated."""
 
 
183
  sample_id = uuid.uuid4().hex[:10]
184
  date = datetime.now(timezone.utc).isoformat()
185
  base = f"sessions/{session_id}/{date[:19].replace(':', '-')}_{sample_id}"
@@ -187,8 +210,11 @@ def log_submission(session_id: str, image: Image.Image, prompt: str,
187
  "session_id": session_id,
188
  "prompt": prompt,
189
  "date": date,
 
190
  "status": status,
191
  }
 
 
192
  if error:
193
  record["error"] = error[:4000]
194
  files = [
@@ -203,8 +229,18 @@ def log_submission(session_id: str, image: Image.Image, prompt: str,
203
 
204
 
205
  # --- Main pipeline ---------------------------------------------------------------
206
- BLOCKED_MSG = "🚫 Prompt forbidden"
207
- FAILED_MSG = "⚠️ Generation failed — please try again."
 
 
 
 
 
 
 
 
 
 
208
 
209
 
210
  def edit_image(image, prompt, model_choice, request: gr.Request,
@@ -215,35 +251,39 @@ def edit_image(image, prompt, model_choice, request: gr.Request,
215
  raise gr.Error("Please describe the change you would like to make.")
216
  prompt = prompt.strip()
217
 
218
- # One id per browser session: every prompt sent from the same tab —
219
- # including blocked ones and retries — is linked together.
 
220
  session_id = getattr(request, "session_hash", None) or uuid.uuid4().hex
 
221
 
222
- status, error_detail, images = "error", None, []
223
  try:
224
- progress(0.1, desc="Checking prompt…")
225
- if ncii_score(prompt) > NCII_THRESHOLD:
226
- status = "blocked"
227
- raise gr.Error(BLOCKED_MSG)
228
-
229
- if not OPENROUTER_API_KEY:
230
- error_detail = "OPENROUTER_API_KEY not configured"
231
- raise gr.Error(BLOCKED_MSG)
232
-
233
- model = MODEL_ROUTES.get(model_choice, EDIT_MODEL)
234
- progress(0.3, desc="Generating…")
235
- images = generate_variants(_to_data_url(image), prompt, 1, model)
236
- status = "generated"
237
- return images, gr.update(visible=True)
238
- except gr.Error:
239
- raise
240
  except Exception as err:
241
- # Full detail goes to the logs; the user always sees the same message.
242
  error_detail = str(err)
243
- print(f"[DEBUG] generation error: {error_detail}", file=sys.stderr)
244
- raise gr.Error(BLOCKED_MSG)
245
  finally:
246
- log_submission(session_id, image, prompt, status, error_detail, images)
 
 
 
 
 
247
 
248
 
249
  # --- Styling: editorial brief — cream paper, grid, ink & orange -----------------
@@ -257,10 +297,13 @@ LAB_CSS = """
257
  --muted: #8f8a7d;
258
  --accent: #ee4f1e;
259
  --grid: rgba(22, 19, 14, 0.06);
 
260
  }
261
 
262
- body, .app, gradio-app {
 
263
  background: var(--paper) !important;
 
264
  }
265
 
266
  .gradio-container {
@@ -386,19 +429,6 @@ button {
386
  color: #fff !important;
387
  }
388
 
389
- /* ---------- like callout ---------- */
390
- #like-callout {
391
- border: 2px solid var(--accent) !important;
392
- background: var(--panel) !important;
393
- text-align: center;
394
- padding: 0.8rem;
395
- font-size: 0.78rem;
396
- letter-spacing: 2.5px;
397
- text-transform: uppercase;
398
- color: var(--ink);
399
- }
400
- #like-callout .accent { color: var(--accent); font-weight: 700; }
401
-
402
  footer { visibility: hidden; }
403
 
404
  /* ---------- privacy ---------- */
@@ -468,22 +498,9 @@ PRIVACY_HTML = """
468
  </div>
469
  """
470
 
471
- # Pin the light theme the design is paper-based and must not invert.
472
- FORCE_LIGHT_HEAD = """
473
- <script>
474
- (function () {
475
- const url = new URL(window.location);
476
- if (url.searchParams.get('__theme') !== 'light') {
477
- url.searchParams.set('__theme', 'light');
478
- window.location.replace(url.href);
479
- }
480
- })();
481
- </script>
482
- """
483
-
484
- # Gradio 6 moved css/head from the Blocks constructor to launch().
485
  GRADIO_MAJOR = int(gr.__version__.split(".")[0])
486
- _style_kwargs = {"css": LAB_CSS, "head": FORCE_LIGHT_HEAD}
487
  _blocks_kwargs = {} if GRADIO_MAJOR >= 6 else dict(_style_kwargs)
488
  _launch_kwargs = dict(_style_kwargs) if GRADIO_MAJOR >= 6 else {}
489
 
@@ -517,16 +534,11 @@ with gr.Blocks(title="Describe It. Done.", fill_width=True, **_blocks_kwargs) as
517
  height=460,
518
  object_fit="contain",
519
  )
520
- like_callout = gr.HTML(
521
- "<div id='like-callout'>Enjoying the results ? "
522
- "<span class='accent'>&hearts; Like this Space</span> 🙂</div>",
523
- visible=False,
524
- )
525
 
526
  submit_btn.click(
527
  fn=edit_image,
528
  inputs=[image_in, prompt_in, model_in],
529
- outputs=[gallery_out, like_callout],
530
  show_progress="full",
531
  )
532
 
 
1
  import base64
2
+ import hashlib
3
  import io
4
  import json
5
  import os
 
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
 
 
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 = {
 
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
  "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 = [
 
229
 
230
 
231
  # --- Main pipeline ---------------------------------------------------------------
232
+ BACKEND_FAILURE_MSG = "⚠️ Backend failure. Please try again later."
233
+
234
+
235
+ def _credible_progress(progress, seconds: float = CREDIBLE_WAIT_SECONDS):
236
+ """Fill ~`seconds` with a moving progress bar so a local (instant) check
237
+ looks like real generation work."""
238
+ phases = ["Analyzing image…", "Generating…", "Rendering…", "Finalizing…"]
239
+ steps = 40
240
+ for i in range(steps):
241
+ frac = (i + 1) / steps
242
+ progress(frac * 0.98, desc=phases[min(len(phases) - 1, int(frac * len(phases)))])
243
+ time.sleep(seconds / steps)
244
 
245
 
246
  def edit_image(image, prompt, model_choice, request: gr.Request,
 
251
  raise gr.Error("Please describe the change you would like to make.")
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
 
289
  # --- Styling: editorial brief — cream paper, grid, ink & orange -----------------
 
297
  --muted: #8f8a7d;
298
  --accent: #ee4f1e;
299
  --grid: rgba(22, 19, 14, 0.06);
300
+ color-scheme: light; /* keep native controls light, no dark flash */
301
  }
302
 
303
+ /* Lock the paper look in every theme so the header never flickers. */
304
+ html, body, .app, gradio-app, .dark {
305
  background: var(--paper) !important;
306
+ color-scheme: light;
307
  }
308
 
309
  .gradio-container {
 
429
  color: #fff !important;
430
  }
431
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  footer { visibility: hidden; }
433
 
434
  /* ---------- privacy ---------- */
 
498
  </div>
499
  """
500
 
501
+ # Gradio 6 moved css from the Blocks constructor to launch().
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  GRADIO_MAJOR = int(gr.__version__.split(".")[0])
503
+ _style_kwargs = {"css": LAB_CSS}
504
  _blocks_kwargs = {} if GRADIO_MAJOR >= 6 else dict(_style_kwargs)
505
  _launch_kwargs = dict(_style_kwargs) if GRADIO_MAJOR >= 6 else {}
506
 
 
534
  height=460,
535
  object_fit="contain",
536
  )
 
 
 
 
 
537
 
538
  submit_btn.click(
539
  fn=edit_image,
540
  inputs=[image_in, prompt_in, model_in],
541
+ outputs=[gallery_out],
542
  show_progress="full",
543
  )
544