FoxLoverAI commited on
Commit
0dfa74d
·
verified ·
1 Parent(s): 2d810de

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +485 -140
app.py CHANGED
@@ -1,154 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
- import numpy as np
3
- import random
4
-
5
- # import spaces #[uncomment to use ZeroGPU]
6
- from diffusers import DiffusionPipeline
7
- import torch
8
-
9
- device = "cuda" if torch.cuda.is_available() else "cpu"
10
- model_repo_id = "stabilityai/sdxl-turbo" # Replace to the model you would like to use
11
-
12
- if torch.cuda.is_available():
13
- torch_dtype = torch.float16
14
- else:
15
- torch_dtype = torch.float32
16
-
17
- pipe = DiffusionPipeline.from_pretrained(model_repo_id, torch_dtype=torch_dtype)
18
- pipe = pipe.to(device)
19
-
20
- MAX_SEED = np.iinfo(np.int32).max
21
- MAX_IMAGE_SIZE = 1024
22
-
23
-
24
- # @spaces.GPU #[uncomment to use ZeroGPU]
25
- def infer(
26
- prompt,
27
- negative_prompt,
28
- seed,
29
- randomize_seed,
30
- width,
31
- height,
32
- guidance_scale,
33
- num_inference_steps,
34
- progress=gr.Progress(track_tqdm=True),
35
- ):
36
- if randomize_seed:
37
- seed = random.randint(0, MAX_SEED)
38
-
39
- generator = torch.Generator().manual_seed(seed)
40
-
41
- image = pipe(
42
- prompt=prompt,
43
- negative_prompt=negative_prompt,
44
- guidance_scale=guidance_scale,
45
- num_inference_steps=num_inference_steps,
46
- width=width,
47
- height=height,
48
- generator=generator,
49
- ).images[0]
50
-
51
- return image, seed
52
-
53
-
54
- examples = [
55
- "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k",
56
- "An astronaut riding a green horse",
57
- "A delicious ceviche cheesecake slice",
58
- ]
59
-
60
- css = """
61
- #col-container {
62
- margin: 0 auto;
63
- max-width: 640px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
  """
66
 
67
- with gr.Blocks(css=css) as demo:
68
- with gr.Column(elem_id="col-container"):
69
- gr.Markdown(" # Text-to-Image Gradio Template")
70
-
71
- with gr.Row():
72
- prompt = gr.Text(
73
- label="Prompt",
74
- show_label=False,
75
- max_lines=1,
76
- placeholder="Enter your prompt",
77
- container=False,
78
- )
 
 
 
 
 
79
 
80
- run_button = gr.Button("Run", scale=0, variant="primary")
 
 
 
 
 
 
 
 
81
 
82
- result = gr.Image(label="Result", show_label=False)
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- with gr.Accordion("Advanced Settings", open=False):
85
- negative_prompt = gr.Text(
86
- label="Negative prompt",
87
- max_lines=1,
88
- placeholder="Enter a negative prompt",
89
- visible=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  )
 
 
 
 
 
 
91
 
92
- seed = gr.Slider(
93
- label="Seed",
94
- minimum=0,
95
- maximum=MAX_SEED,
96
- step=1,
97
- value=0,
 
 
 
 
 
98
  )
99
 
100
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
101
-
102
- with gr.Row():
103
- width = gr.Slider(
104
- label="Width",
105
- minimum=256,
106
- maximum=MAX_IMAGE_SIZE,
107
- step=32,
108
- value=1024, # Replace with defaults that work for your model
109
- )
110
-
111
- height = gr.Slider(
112
- label="Height",
113
- minimum=256,
114
- maximum=MAX_IMAGE_SIZE,
115
- step=32,
116
- value=1024, # Replace with defaults that work for your model
117
- )
118
-
119
- with gr.Row():
120
- guidance_scale = gr.Slider(
121
- label="Guidance scale",
122
- minimum=0.0,
123
- maximum=10.0,
124
- step=0.1,
125
- value=0.0, # Replace with defaults that work for your model
126
- )
127
-
128
- num_inference_steps = gr.Slider(
129
- label="Number of inference steps",
130
- minimum=1,
131
- maximum=50,
132
- step=1,
133
- value=2, # Replace with defaults that work for your model
134
- )
135
-
136
- gr.Examples(examples=examples, inputs=[prompt])
137
- gr.on(
138
- triggers=[run_button.click, prompt.submit],
139
- fn=infer,
140
- inputs=[
141
- prompt,
142
- negative_prompt,
143
- seed,
144
- randomize_seed,
145
- width,
146
- height,
147
- guidance_scale,
148
- num_inference_steps,
149
- ],
150
- outputs=[result, seed],
151
  )
152
 
 
 
 
 
153
  if __name__ == "__main__":
154
- demo.launch()
 
1
+ import base64
2
+ import io
3
+ import json
4
+ import os
5
+ import sys
6
+ import threading
7
+ import time
8
+ import traceback
9
+ import uuid
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
+ from datetime import datetime, timezone
12
+
13
  import gradio as gr
14
+ import requests
15
+ from PIL import Image
16
+
17
+ try:
18
+ from huggingface_hub import batch_bucket_files
19
+ except ImportError:
20
+ batch_bucket_files = None
21
+
22
+ # --- Config (secrets only, never hardcoded) ----------------------------------
23
+ HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
24
+ BUCKET_ID = os.environ.get("BUCKET_ID", "").strip()
25
+ OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "").strip()
26
+
27
+ OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images"
28
+ EDIT_MODEL = "black-forest-labs/flux.2-klein-4b"
29
+ GUARD_MODEL = "hfmlsoc/ncii-guard-v02"
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
+
42
+ # --- Prompt guard -------------------------------------------------------------
43
+ _guard = None
44
+ _guard_lock = threading.Lock()
45
+ _guard_failed = False
46
+
47
+
48
+ def _load_guard():
49
+ """Load the classifier once; safe to call from several threads."""
50
+ global _guard, _guard_failed
51
+ if _guard is not None:
52
+ return _guard
53
+ with _guard_lock:
54
+ if _guard is None and not _guard_failed:
55
+ try:
56
+ from transformers import pipeline
57
+ _guard = pipeline("text-classification", model=GUARD_MODEL)
58
+ except Exception:
59
+ _guard_failed = True
60
+ print(f"[DEBUG] guard load failed:\n{traceback.format_exc()}", file=sys.stderr)
61
+ return _guard
62
+
63
+
64
+ def ncii_score(prompt: str) -> float:
65
+ """Return the ncii probability for a prompt. Fails closed on any error."""
66
+ clf = _load_guard()
67
+ if clf is None:
68
+ # Safety system unavailable -> refuse to generate rather than bypass it.
69
+ raise gr.Error("Safety system is warming up — please try again in a minute.")
70
+ try:
71
+ result = clf(prompt[:2048])[0]
72
+ label = str(result.get("label", "")).strip().lower()
73
+ score = float(result.get("score", 1.0))
74
+ except Exception:
75
+ print(f"[DEBUG] guard inference failed:\n{traceback.format_exc()}", file=sys.stderr)
76
+ raise gr.Error("Safety check failed — please try again.")
77
+ return score if label == "ncii" else 1.0 - score
78
+
79
+
80
+ # Warm the model in the background so the first user doesn't pay the load time.
81
+ threading.Thread(target=_load_guard, daemon=True).start()
82
+
83
+
84
+ # --- Image encoding / OpenRouter ----------------------------------------------
85
+ def _to_data_url(image: Image.Image) -> str:
86
+ img = image.convert("RGB")
87
+ if max(img.size) > MAX_SIDE:
88
+ ratio = MAX_SIDE / max(img.size)
89
+ img = img.resize((round(img.width * ratio), round(img.height * ratio)), Image.LANCZOS)
90
+ buf = io.BytesIO()
91
+ img.save(buf, format="PNG")
92
+ return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
93
+
94
+
95
+ def _decode_result(item: dict) -> Image.Image:
96
+ if item.get("b64_json"):
97
+ return Image.open(io.BytesIO(base64.b64decode(item["b64_json"])))
98
+ url = item.get("url") or (item.get("image_url") or {}).get("url", "")
99
+ if url.startswith("data:"):
100
+ return Image.open(io.BytesIO(base64.b64decode(url.split(",", 1)[1])))
101
+ if url:
102
+ resp = requests.get(url, timeout=60)
103
+ resp.raise_for_status()
104
+ return Image.open(io.BytesIO(resp.content))
105
+ raise ValueError(f"No image payload in response item: {list(item.keys())}")
106
+
107
+
108
+ def _generate_one(data_url: str, prompt: str, model: str = EDIT_MODEL,
109
+ attempts: int = 2) -> Image.Image:
110
+ payload = {
111
+ "model": model,
112
+ "prompt": prompt,
113
+ "input_references": [{"type": "image_url", "image_url": {"url": data_url}}],
114
+ }
115
+ headers = {
116
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
117
+ "Content-Type": "application/json",
118
+ }
119
+ last_err = None
120
+ for attempt in range(attempts):
121
+ try:
122
+ resp = requests.post(
123
+ OPENROUTER_IMAGES_URL, json=payload, headers=headers, timeout=REQUEST_TIMEOUT
124
+ )
125
+ if resp.status_code != 200:
126
+ snippet = resp.text[:300]
127
+ raise RuntimeError(f"OpenRouter {resp.status_code}: {snippet}")
128
+ data = resp.json().get("data") or []
129
+ if not data:
130
+ raise RuntimeError("OpenRouter returned an empty result.")
131
+ return _decode_result(data[0])
132
+ except Exception as err:
133
+ last_err = err
134
+ if attempt < attempts - 1:
135
+ time.sleep(1.5 * (attempt + 1))
136
+ raise last_err
137
+
138
+
139
+ def generate_variants(data_url: str, prompt: str, n: int, model: str = EDIT_MODEL) -> list:
140
+ """n independent requests in parallel — guarantees n distinct variants
141
+ regardless of provider support for the `n` parameter."""
142
+ images, errors = [], []
143
+ with ThreadPoolExecutor(max_workers=n) as pool:
144
+ futures = [pool.submit(_generate_one, data_url, prompt, model) for _ in range(n)]
145
+ for future in as_completed(futures):
146
+ try:
147
+ images.append(future.result())
148
+ except Exception as err:
149
+ errors.append(err)
150
+ print(f"[DEBUG] variant failed: {err}", file=sys.stderr)
151
+ if not images:
152
+ raise gr.Error("Generation failed — the model may be busy, please retry.")
153
+ if errors:
154
+ gr.Warning(f"{len(images)}/{n} variants completed.")
155
+ return images
156
+
157
+
158
+ # --- Logging -------------------------------------------------------------------
159
+ def log_submission(session_id: str, image: Image.Image, prompt: str):
160
+ """Fire-and-forget export of {image, prompt, date, session_id} to the bucket."""
161
+ if not logging_enabled:
162
+ return
163
+
164
+ def _worker():
165
+ try:
166
+ sample_id = uuid.uuid4().hex[:10]
167
+ date = datetime.now(timezone.utc).isoformat()
168
+ buf = io.BytesIO()
169
+ image.convert("RGB").save(buf, format="PNG")
170
+ record = {"session_id": session_id, "prompt": prompt, "date": date}
171
+ base = f"sessions/{session_id}/{date[:19].replace(':', '-')}_{sample_id}"
172
+ batch_bucket_files(
173
+ BUCKET_ID,
174
+ add=[
175
+ (buf.getvalue(), f"{base}.png"),
176
+ (json.dumps(record).encode(), f"{base}.json"),
177
+ ],
178
+ token=HF_TOKEN,
179
+ )
180
+ except Exception:
181
+ # logging must never take the app down — degrade to no logging
182
+ print(f"[DEBUG] log_submission failed:\n{traceback.format_exc()}", file=sys.stderr)
183
+
184
+ threading.Thread(target=_worker, daemon=True).start()
185
+
186
+
187
+ # --- Main pipeline ---------------------------------------------------------------
188
+ def edit_image(image, prompt, model_choice, request: gr.Request,
189
+ progress=gr.Progress()):
190
+ if image is None:
191
+ raise gr.Error("Please upload an image first.")
192
+ if not prompt or not prompt.strip():
193
+ raise gr.Error("Please describe the change you would like to make.")
194
+ prompt = prompt.strip()
195
+
196
+ # One id per browser session: every prompt sent from the same tab —
197
+ # including blocked ones and retries — is linked together.
198
+ session_id = getattr(request, "session_hash", None) or uuid.uuid4().hex
199
+ log_submission(session_id, image, prompt)
200
+
201
+ progress(0.1, desc="Checking prompt…")
202
+ if ncii_score(prompt) > NCII_THRESHOLD:
203
+ raise gr.Error("🚫 Prompt forbidden")
204
+
205
+ if not OPENROUTER_API_KEY:
206
+ raise gr.Error("Server is not configured yet (missing API key).")
207
+
208
+ model = MODEL_ROUTES.get(model_choice, EDIT_MODEL)
209
+ progress(0.3, desc="Generating…")
210
+ images = generate_variants(_to_data_url(image), prompt, 1, model)
211
+
212
+ return images, gr.update(visible=True)
213
+
214
+
215
+ # --- Styling: editorial brief — cream paper, grid, ink & orange -----------------
216
+ LAB_CSS = """
217
+ @import url('https://fonts.googleapis.com/css2?family=Archivo+Black&family=JetBrains+Mono:wght@400;500;700&display=swap');
218
+
219
+ :root {
220
+ --paper: #f4f0e6;
221
+ --panel: #fbf9f2;
222
+ --ink: #16130e;
223
+ --muted: #8f8a7d;
224
+ --accent: #ee4f1e;
225
+ --grid: rgba(22, 19, 14, 0.06);
226
+ }
227
+
228
+ body, .app, gradio-app {
229
+ background: var(--paper) !important;
230
+ }
231
+
232
+ .gradio-container {
233
+ background-color: var(--paper) !important;
234
+ background-image:
235
+ linear-gradient(var(--grid) 1px, transparent 1px),
236
+ linear-gradient(90deg, var(--grid) 1px, transparent 1px);
237
+ background-size: 44px 44px;
238
+ font-family: 'JetBrains Mono', monospace !important;
239
+ color: var(--ink) !important;
240
+ max-width: 1180px !important;
241
+ margin: 0 auto !important;
242
+ }
243
+
244
+ /* ---------- header frame ---------- */
245
+ #brief-frame {
246
+ position: relative;
247
+ border: 2px solid var(--ink);
248
+ background: var(--paper);
249
+ padding: 1.1rem 1.6rem 0.4rem;
250
+ margin: 1.6rem 0 1.8rem;
251
+ }
252
+ #brief-frame .tick {
253
+ position: absolute;
254
+ background: var(--ink);
255
+ }
256
+ #brief-frame .tick.t1 { top: -12px; left: 18%; width: 2px; height: 24px; }
257
+ #brief-frame .tick.t2 { top: -12px; right: 8%; width: 2px; height: 24px; }
258
+ #brief-frame .tick.t3 { bottom: -12px; left: 40%; width: 2px; height: 24px; }
259
+ #brief-frame .tick.t4 { top: 30%; left: -12px; width: 24px; height: 2px; }
260
+ #brief-frame .tick.t5 { top: 62%; right: -12px; width: 24px; height: 2px; }
261
+
262
+ .brief-kicker {
263
+ display: flex;
264
+ justify-content: space-between;
265
+ gap: 1rem;
266
+ color: var(--ink);
267
+ font-size: 0.72rem;
268
+ font-weight: 700;
269
+ letter-spacing: 4px;
270
+ text-transform: uppercase;
271
+ padding-bottom: 0.9rem;
272
+ }
273
+ .brief-kicker span { color: var(--ink) !important; }
274
+ .brief-kicker span.dim { color: var(--muted) !important; font-weight: 500; }
275
+
276
+ .brief-headline {
277
+ font-family: 'Archivo Black', 'JetBrains Mono', sans-serif;
278
+ font-size: clamp(2.1rem, 5.2vw, 3.6rem);
279
+ line-height: 1.04;
280
+ letter-spacing: 1px;
281
+ text-transform: uppercase;
282
+ margin: 1.4rem 0 1rem;
283
+ color: var(--ink);
284
+ }
285
+ .brief-headline .accent { color: var(--accent); }
286
+
287
+ .brief-sub {
288
+ font-size: 0.8rem;
289
+ letter-spacing: 3.5px;
290
+ text-transform: uppercase;
291
+ color: var(--muted);
292
+ margin: 0 0 1.6rem;
293
+ }
294
+
295
+ /* floating component labels (e.g. on the image inputs) */
296
+ .block label.float, .block .label {
297
+ background: var(--ink) !important;
298
+ color: var(--paper) !important;
299
+ border-radius: 0 !important;
300
+ }
301
+
302
+ /* ---------- panels & fields ---------- */
303
+ .gr-panel, .block, .form {
304
+ background: var(--panel) !important;
305
+ border: 2px solid var(--ink) !important;
306
+ border-radius: 0 !important;
307
+ box-shadow: none !important;
308
+ }
309
+
310
+ textarea, input, select {
311
+ background: var(--panel) !important;
312
+ color: var(--ink) !important;
313
+ font-family: 'JetBrains Mono', monospace !important;
314
+ border: 2px solid var(--ink) !important;
315
+ border-radius: 0 !important;
316
+ }
317
+ textarea:focus, input:focus { border-color: var(--accent) !important; }
318
+
319
+ label, label span, .gr-check-radio span, span[data-testid="block-info"] {
320
+ color: var(--ink) !important;
321
+ font-family: 'JetBrains Mono', monospace !important;
322
+ font-size: 0.72rem !important;
323
+ font-weight: 700 !important;
324
+ text-transform: uppercase;
325
+ letter-spacing: 2px;
326
+ }
327
+
328
+ button {
329
+ font-family: 'JetBrains Mono', monospace !important;
330
+ font-weight: 700 !important;
331
+ text-transform: uppercase;
332
+ letter-spacing: 2.5px;
333
+ border-radius: 0 !important;
334
+ transition: all 0.15s ease-in-out;
335
+ }
336
+
337
+ #submit-btn {
338
+ background: var(--ink) !important;
339
+ color: var(--paper) !important;
340
+ border: 2px solid var(--ink) !important;
341
+ padding: 0.9rem !important;
342
+ font-size: 0.9rem !important;
343
+ }
344
+ #submit-btn:hover {
345
+ background: var(--accent) !important;
346
+ border-color: var(--accent) !important;
347
+ color: #fff !important;
348
+ }
349
+
350
+ /* ---------- like callout ---------- */
351
+ #like-callout {
352
+ border: 2px solid var(--accent) !important;
353
+ background: var(--panel) !important;
354
+ text-align: center;
355
+ padding: 0.8rem;
356
+ font-size: 0.78rem;
357
+ letter-spacing: 2.5px;
358
+ text-transform: uppercase;
359
+ color: var(--ink);
360
+ }
361
+ #like-callout .accent { color: var(--accent); font-weight: 700; }
362
+
363
+ footer { visibility: hidden; }
364
+
365
+ /* ---------- privacy ---------- */
366
+ #privacy-footer {
367
+ text-align: center;
368
+ font-size: 0.7rem;
369
+ color: var(--muted);
370
+ margin-top: 2rem;
371
+ letter-spacing: 1.5px;
372
+ text-transform: uppercase;
373
+ }
374
+ #privacy-footer a { color: var(--muted); text-decoration: underline; cursor: pointer; }
375
+
376
+ #privacy-modal {
377
+ display: none;
378
+ position: fixed;
379
+ top: 0; left: 0; width: 100%; height: 100%;
380
+ background: rgba(22, 19, 14, 0.6);
381
+ z-index: 9999;
382
+ align-items: center;
383
+ justify-content: center;
384
+ }
385
+ #privacy-modal.open { display: flex; }
386
+ #privacy-modal-box {
387
+ background: var(--paper);
388
+ border: 2px solid var(--ink);
389
+ padding: 2rem;
390
+ max-width: 480px;
391
+ font-family: 'JetBrains Mono', monospace;
392
+ font-size: 0.8rem;
393
+ line-height: 1.55;
394
+ }
395
+ #privacy-modal-box button {
396
+ margin-top: 1rem;
397
+ background: var(--ink);
398
+ color: var(--paper);
399
+ border: 2px solid var(--ink);
400
+ padding: 0.5rem 1.4rem;
401
  }
402
  """
403
 
404
+ HEADER_HTML = """
405
+ <div id="brief-frame">
406
+ <span class="tick t1"></span><span class="tick t2"></span><span class="tick t3"></span>
407
+ <span class="tick t4"></span><span class="tick t5"></span>
408
+ <div class="brief-kicker">
409
+ <span>WanGen &middot; Image Editing Studio</span>
410
+ <span class="dim">Open Models &middot; Free For All</span>
411
+ </div>
412
+ <h1 class="brief-headline">Describe It. <span class="accent">Done.</span></h1>
413
+ <p class="brief-sub">Multi-model editing &mdash; for the beauty of open source.</p>
414
+ </div>
415
+ """
416
+
417
+ PRIVACY_HTML = """
418
+ <p id='privacy-footer'>
419
+ <a onclick="document.getElementById('privacy-modal').classList.add('open')">Privacy Policy</a>
420
+ </p>
421
 
422
+ <div id='privacy-modal'>
423
+ <div id='privacy-modal-box'>
424
+ <strong>Privacy Policy</strong><br><br>
425
+ Please do not upload personal information, or images you do not have the right to use.<br><br>
426
+ No personal data beyond what you explicitly submitted is collected. Only the data required for the system to function &mdash; your prompt and the submitted image &mdash; is processed, for AI research purposes.<br><br>
427
+ <button onclick="document.getElementById('privacy-modal').classList.remove('open')">Close</button>
428
+ </div>
429
+ </div>
430
+ """
431
 
432
+ # Pin the light theme — the design is paper-based and must not invert.
433
+ FORCE_LIGHT_HEAD = """
434
+ <script>
435
+ (function () {
436
+ const url = new URL(window.location);
437
+ if (url.searchParams.get('__theme') !== 'light') {
438
+ url.searchParams.set('__theme', 'light');
439
+ window.location.replace(url.href);
440
+ }
441
+ })();
442
+ </script>
443
+ """
444
 
445
+ # Gradio 6 moved css/head from the Blocks constructor to launch().
446
+ GRADIO_MAJOR = int(gr.__version__.split(".")[0])
447
+ _style_kwargs = {"css": LAB_CSS, "head": FORCE_LIGHT_HEAD}
448
+ _blocks_kwargs = {} if GRADIO_MAJOR >= 6 else dict(_style_kwargs)
449
+ _launch_kwargs = dict(_style_kwargs) if GRADIO_MAJOR >= 6 else {}
450
+
451
+ with gr.Blocks(title="Describe It. Done.", **_blocks_kwargs) as demo:
452
+ gr.HTML(HEADER_HTML)
453
+
454
+ with gr.Row(equal_height=False):
455
+ with gr.Column(scale=5):
456
+ image_in = gr.Image(
457
+ type="pil",
458
+ label="Input Image — drop, paste or upload",
459
+ sources=["upload", "clipboard"],
460
+ height=320,
461
+ )
462
+ prompt_in = gr.Textbox(
463
+ label="What changes would you like to make ?",
464
+ placeholder="e.g. change the background to a forest at dusk",
465
+ lines=3,
466
  )
467
+ model_in = gr.Dropdown(
468
+ choices=MODEL_CHOICES,
469
+ value="Auto-Routing",
470
+ label="Model",
471
+ )
472
+ submit_btn = gr.Button("Submit", elem_id="submit-btn")
473
 
474
+ with gr.Column(scale=5):
475
+ gallery_out = gr.Gallery(
476
+ label="Output",
477
+ columns=1,
478
+ height=460,
479
+ object_fit="contain",
480
+ )
481
+ like_callout = gr.HTML(
482
+ "<div id='like-callout'>Enjoying the results ? "
483
+ "<span class='accent'>&hearts; Like this Space</span> 🙂</div>",
484
+ visible=False,
485
  )
486
 
487
+ submit_btn.click(
488
+ fn=edit_image,
489
+ inputs=[image_in, prompt_in, model_in],
490
+ outputs=[gallery_out, like_callout],
491
+ show_progress="full",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  )
493
 
494
+ gr.HTML(PRIVACY_HTML)
495
+
496
+ demo.queue(max_size=20, default_concurrency_limit=4)
497
+
498
  if __name__ == "__main__":
499
+ demo.launch(share=False, **_launch_kwargs)