Vector857 commited on
Commit
f3e330f
Β·
verified Β·
1 Parent(s): d098c5c

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +731 -0
app.py ADDED
@@ -0,0 +1,731 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import math
4
+ import time
5
+ import random
6
+ import numpy as np
7
+ from typing import Iterable
8
+
9
+ import torch
10
+ from PIL import Image
11
+ import gradio as gr
12
+ import spaces
13
+ from diffusers import DiffusionPipeline, FlowMatchEulerDiscreteScheduler
14
+ from huggingface_hub import HfFileSystem, ModelCard
15
+ from gradio.themes import Soft
16
+ from gradio.themes.utils import colors, fonts, sizes
17
+
18
+ # ── THEME ────────────────────────────────────────────────────────────────────
19
+
20
+ THEME_PRESETS = {
21
+ "orange-red": {"primary": "#c2410c", "secondary": "#ea580c"},
22
+ "violet": {"primary": "#7c3aed", "secondary": "#8b5cf6"},
23
+ "ocean": {"primary": "#0369a1", "secondary": "#0ea5e9"},
24
+ "emerald": {"primary": "#047857", "secondary": "#10b981"},
25
+ "rose": {"primary": "#be185d", "secondary": "#ec4899"},
26
+ "amber": {"primary": "#b45309", "secondary": "#f59e0b"},
27
+ "cyan": {"primary": "#0e7490", "secondary": "#06b6d4"},
28
+ }
29
+
30
+ def make_color(name: str, hex_val: str) -> colors.Color:
31
+ """Generate a Gradio Color object from a single hex value (simplified ramp)."""
32
+ return colors.Color(
33
+ name=name,
34
+ c50=hex_val + "15",
35
+ c100=hex_val + "25",
36
+ c200=hex_val + "40",
37
+ c300=hex_val + "60",
38
+ c400=hex_val + "80",
39
+ c500=hex_val,
40
+ c600=hex_val,
41
+ c700=hex_val,
42
+ c800=hex_val,
43
+ c900=hex_val,
44
+ c950=hex_val,
45
+ )
46
+
47
+ class DRexTheme(Soft):
48
+ def __init__(
49
+ self,
50
+ primary_hue=colors.gray,
51
+ accent_hex: str = "#c2410c",
52
+ *,
53
+ neutral_hue=colors.slate,
54
+ text_size=sizes.text_md,
55
+ font: Iterable[fonts.Font | str] = (
56
+ fonts.GoogleFont("Space Grotesk"), "Arial", "sans-serif"
57
+ ),
58
+ font_mono: Iterable[fonts.Font | str] = (
59
+ fonts.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"
60
+ ),
61
+ ):
62
+ super().__init__(
63
+ primary_hue=primary_hue,
64
+ neutral_hue=neutral_hue,
65
+ text_size=text_size,
66
+ font=font,
67
+ font_mono=font_mono,
68
+ )
69
+ a = accent_hex
70
+ super().set(
71
+ # backgrounds
72
+ background_fill_primary="*primary_50",
73
+ background_fill_primary_dark="*primary_900",
74
+ body_background_fill="#0f0d0c",
75
+ body_background_fill_dark="#0b0a09",
76
+ # buttons
77
+ button_primary_text_color="white",
78
+ button_primary_text_color_hover="white",
79
+ button_primary_background_fill=a,
80
+ button_primary_background_fill_hover=a,
81
+ button_primary_background_fill_dark=a,
82
+ button_primary_background_fill_hover_dark=a,
83
+ button_secondary_background_fill="*primary_200",
84
+ button_secondary_background_fill_hover="*primary_300",
85
+ # slider
86
+ slider_color=a,
87
+ slider_color_dark=a,
88
+ # block
89
+ block_title_text_weight="600",
90
+ block_border_width="1px",
91
+ block_shadow="none",
92
+ block_label_background_fill="*primary_100",
93
+ # input
94
+ input_background_fill="#1a1816",
95
+ input_background_fill_dark="#1a1816",
96
+ input_border_color="#272422",
97
+ input_border_color_dark="#272422",
98
+ )
99
+
100
+ drex_theme = DRexTheme(accent_hex="#c2410c")
101
+
102
+ # ── GPU / DEVICE ─────────────────────────────────────────────────────────────
103
+
104
+ device = "cuda" if torch.cuda.is_available() else "cpu"
105
+ dtype = torch.bfloat16
106
+
107
+ print(f"[D-REX] Using device: {device}")
108
+
109
+ # ── LORAS ────────────────────────────────────────────────────────────────────
110
+
111
+ loras = [
112
+ {
113
+ "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Studio-Realism/resolve/main/images/2.png",
114
+ "title": "Studio Realism",
115
+ "repo": "prithivMLmods/Qwen-Image-Studio-Realism",
116
+ "weights": "qwen-studio-realism.safetensors",
117
+ "trigger_word": "Studio Realism",
118
+ },
119
+ {
120
+ "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Sketch-Smudge/resolve/main/images/1.png",
121
+ "title": "Sketch Smudge",
122
+ "repo": "prithivMLmods/Qwen-Image-Sketch-Smudge",
123
+ "weights": "qwen-sketch-smudge.safetensors",
124
+ "trigger_word": "Sketch Smudge",
125
+ },
126
+ {
127
+ "image": "https://huggingface.co/Shakker-Labs/AWPortrait-QW/resolve/main/images/08fdaf6b644b61136340d5c908ca37993e47f34cdbe2e8e8251c4c72.jpg",
128
+ "title": "AWPortrait QW",
129
+ "repo": "Shakker-Labs/AWPortrait-QW",
130
+ "weights": "AWPortrait-QW_1.0.safetensors",
131
+ "trigger_word": "Portrait",
132
+ },
133
+ {
134
+ "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Anime-LoRA/resolve/main/images/1.png",
135
+ "title": "Qwen Anime",
136
+ "repo": "prithivMLmods/Qwen-Image-Anime-LoRA",
137
+ "weights": "qwen-anime.safetensors",
138
+ "trigger_word": "Qwen Anime",
139
+ },
140
+ {
141
+ "image": "https://huggingface.co/flymy-ai/qwen-image-realism-lora/resolve/main/assets/flymy_realism.png",
142
+ "title": "Image Realism",
143
+ "repo": "flymy-ai/qwen-image-realism-lora",
144
+ "weights": "flymy_realism.safetensors",
145
+ "trigger_word": "Super Realism Portrait",
146
+ },
147
+ {
148
+ "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Fragmented-Portraiture/resolve/main/images/3.png",
149
+ "title": "Fragmented Portraiture",
150
+ "repo": "prithivMLmods/Qwen-Image-Fragmented-Portraiture",
151
+ "weights": "qwen-fragmented-portraiture.safetensors",
152
+ "trigger_word": "Fragmented Portraiture",
153
+ },
154
+ {
155
+ "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Synthetic-Face/resolve/main/images/2.png",
156
+ "title": "Synthetic Face",
157
+ "repo": "prithivMLmods/Qwen-Image-Synthetic-Face",
158
+ "weights": "qwen-synthetic-face.safetensors",
159
+ "trigger_word": "Synthetic Face",
160
+ },
161
+ {
162
+ "image": "https://huggingface.co/itspoidaman/qwenglitch/resolve/main/images/GyZTwJIbkAAhS4h.jpeg",
163
+ "title": "Qwen Glitch",
164
+ "repo": "itspoidaman/qwenglitch",
165
+ "weights": "qwenglitch1.safetensors",
166
+ "trigger_word": "qwenglitch",
167
+ },
168
+ {
169
+ "image": "https://huggingface.co/alfredplpl/qwen-image-modern-anime-lora/resolve/main/sample1.jpg",
170
+ "title": "Modern Anime",
171
+ "repo": "alfredplpl/qwen-image-modern-anime-lora",
172
+ "weights": "lora.safetensors",
173
+ "trigger_word": "Japanese modern anime style",
174
+ },
175
+ ]
176
+
177
+ # ── MODEL INIT ────────────────────────────────────────────────────────────────
178
+
179
+ scheduler_config = {
180
+ "base_image_seq_len": 256,
181
+ "base_shift": math.log(3),
182
+ "invert_sigmas": False,
183
+ "max_image_seq_len": 8192,
184
+ "max_shift": math.log(3),
185
+ "num_train_timesteps": 1000,
186
+ "shift": 1.0,
187
+ "shift_terminal": None,
188
+ "stochastic_sampling": False,
189
+ "time_shift_type": "exponential",
190
+ "use_beta_sigmas": False,
191
+ "use_dynamic_shifting": True,
192
+ "use_exponential_sigmas": False,
193
+ "use_karras_sigmas": False,
194
+ }
195
+
196
+ scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config)
197
+
198
+ # ZeroGPU: load on CPU β€” moved to cuda inside @spaces.GPU function only
199
+ pipe = DiffusionPipeline.from_pretrained(
200
+ "Qwen/Qwen-Image", scheduler=scheduler, torch_dtype=dtype
201
+ )
202
+
203
+ LIGHTNING_REPO = "lightx2v/Qwen-Image-Lightning"
204
+ LIGHTNING_WEIGHT = "Qwen-Image-Lightning-8steps-V1.0.safetensors"
205
+ MAX_SEED = np.iinfo(np.int32).max
206
+
207
+ ENHANCE_SUFFIXES = [
208
+ ", ultra detailed, cinematic lighting, 8k resolution, professional photography",
209
+ ", masterpiece, intricate details, dramatic composition, volumetric light",
210
+ ", photorealistic, sharp focus, studio lighting, high contrast, award winning",
211
+ ", concept art, highly detailed, trending on artstation, vivid colors, epic",
212
+ ", hyperrealistic, golden hour lighting, bokeh, atmospheric depth, stunning",
213
+ ]
214
+
215
+ # ── HELPERS ───────────────────────────────────────────────────────────────────
216
+
217
+ def aspect_to_wh(aspect: str):
218
+ mapping = {
219
+ "1:1": (1024, 1024), "16:9": (1152, 640), "9:16": (640, 1152),
220
+ "4:3": (1024, 768), "3:4": (768, 1024), "3:2": (1024, 688),
221
+ "2:3": (688, 1024),
222
+ }
223
+ return mapping.get(aspect, (1024, 1024))
224
+
225
+ def build_metadata_html(model_title: str, seed: int, steps: int, cfg: float, aspect: str) -> str:
226
+ w, h = aspect_to_wh(aspect)
227
+ return f"""
228
+ <div style="
229
+ display:grid;grid-template-columns:repeat(5,1fr);gap:8px;
230
+ background:#131110;border:1px solid #272422;border-radius:8px;
231
+ padding:10px 14px;font-family:'JetBrains Mono',monospace;margin-top:6px">
232
+ <div style="text-align:center">
233
+ <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{model_title.split()[0]}</div>
234
+ <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">MODEL</div>
235
+ </div>
236
+ <div style="text-align:center">
237
+ <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{seed}</div>
238
+ <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">SEED</div>
239
+ </div>
240
+ <div style="text-align:center">
241
+ <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{steps}</div>
242
+ <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">STEPS</div>
243
+ </div>
244
+ <div style="text-align:center">
245
+ <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{cfg:.1f}</div>
246
+ <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">CFG</div>
247
+ </div>
248
+ <div style="text-align:center">
249
+ <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{w}Γ—{h}</div>
250
+ <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">SIZE</div>
251
+ </div>
252
+ </div>"""
253
+
254
+ def format_history_html(history: list) -> str:
255
+ if not history:
256
+ return "<div style='font-size:11px;font-family:monospace;color:#524e4a;padding:12px;text-align:center'>no history yet</div>"
257
+ rows = ""
258
+ for h in reversed(history[-10:]):
259
+ rows += f"""
260
+ <div style="padding:6px 8px;background:#1a1816;border:1px solid #272422;
261
+ border-radius:5px;margin-bottom:5px;cursor:pointer"
262
+ onclick="document.querySelector('textarea').value='{h['prompt'].replace("'","")}'">
263
+ <div style="font-size:10px;font-family:monospace;color:#9a9088;
264
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{h['prompt']}</div>
265
+ <div style="font-size:9px;font-family:monospace;color:#524e4a;margin-top:2px">{h['model']} Β· {h['time']}</div>
266
+ </div>"""
267
+ return f"<div style='max-height:180px;overflow-y:auto'>{rows}</div>"
268
+
269
+ # ── CORE LOGIC ────────────────────────────────────────────────────────────────
270
+
271
+ @spaces.GPU(duration=120)
272
+ def generate(
273
+ prompt, neg_prompt, cfg, steps, selected_index,
274
+ randomize_seed, seed, aspect, lora_scale, speed_mode,
275
+ history_state,
276
+ oauth_token: gr.OAuthToken | None = None,
277
+ progress=gr.Progress(track_tqdm=True),
278
+ ):
279
+ if oauth_token is None:
280
+ raise gr.Error("Please sign in with your HuggingFace account to generate images.")
281
+ if selected_index is None:
282
+ raise gr.Error("Select a LoRA from the gallery first.")
283
+ if not prompt.strip():
284
+ raise gr.Error("Write a prompt before generating.")
285
+
286
+ lora = loras[selected_index]
287
+ trigger = lora["trigger_word"]
288
+ prompt_in = f"{trigger} {prompt}" if trigger else prompt
289
+
290
+ # ZeroGPU: LoRA loading must happen inside GPU scope
291
+ pipe.to("cuda")
292
+ pipe.unload_lora_weights()
293
+
294
+ if speed_mode == "Fast Β· 8 steps":
295
+ pipe.load_lora_weights(LIGHTNING_REPO, weight_name=LIGHTNING_WEIGHT, adapter_name="lightning")
296
+ pipe.load_lora_weights(lora["repo"], weight_name=lora.get("weights"), low_cpu_mem_usage=True, adapter_name="style")
297
+ pipe.set_adapters(["lightning", "style"], adapter_weights=[1.0, lora_scale])
298
+ else:
299
+ pipe.load_lora_weights(lora["repo"], weight_name=lora.get("weights"), low_cpu_mem_usage=True, adapter_name="style")
300
+ pipe.set_adapters(["style"], adapter_weights=[lora_scale])
301
+
302
+ if randomize_seed:
303
+ seed = random.randint(0, MAX_SEED)
304
+
305
+ w, h = aspect_to_wh(aspect)
306
+
307
+ generator = torch.Generator(device="cuda").manual_seed(seed)
308
+ image = pipe(
309
+ prompt=prompt_in,
310
+ negative_prompt=neg_prompt,
311
+ num_inference_steps=steps,
312
+ true_cfg_scale=cfg,
313
+ width=w,
314
+ height=h,
315
+ generator=generator,
316
+ ).images[0]
317
+
318
+ pipe.to("cpu")
319
+ torch.cuda.empty_cache()
320
+
321
+ # update history
322
+ history_state = history_state or []
323
+ history_state.append({
324
+ "prompt": prompt[:80],
325
+ "model": lora["title"],
326
+ "time": time.strftime("%H:%M"),
327
+ })
328
+ history_state = history_state[-20:]
329
+
330
+ meta_html = build_metadata_html(lora["title"], seed, steps, cfg, aspect)
331
+ history_html = format_history_html(history_state)
332
+
333
+ return image, seed, meta_html, history_html, history_state
334
+
335
+ def enhance_prompt(prompt: str) -> str:
336
+ if not prompt.strip():
337
+ return prompt
338
+ suffix = random.choice(ENHANCE_SUFFIXES)
339
+ base = prompt.rstrip(".").rstrip(",").strip()
340
+ return base + suffix
341
+
342
+ def on_lora_select(evt: gr.SelectData, aspect):
343
+ lora = loras[evt.index]
344
+ placeholder = f"Describe your image for {lora['title']}..."
345
+ info_md = f"### [{lora['repo']}](https://huggingface.co/{lora['repo']}) βœ…"
346
+ new_aspect = aspect
347
+ if "aspect" in lora:
348
+ new_aspect = {"portrait": "9:16", "landscape": "16:9"}.get(lora["aspect"], aspect)
349
+ return gr.update(placeholder=placeholder), info_md, evt.index, new_aspect
350
+
351
+ def on_speed_change(speed):
352
+ if speed == "Fast Β· 8 steps":
353
+ return gr.update(value="Fast Β· 8 steps with Lightning LoRA"), 8, 1.0
354
+ return gr.update(value="Base Β· 50 steps β€” best quality"), 50, 4.0
355
+
356
+ def fetch_hf_lora(link: str):
357
+ parts = link.strip("/").split("/")
358
+ if len(parts) < 2:
359
+ raise ValueError("Invalid repo path.")
360
+ repo = "/".join(parts[-2:])
361
+ card = ModelCard.load(repo)
362
+ base = card.data.get("base_model", "")
363
+ bases = base if isinstance(base, list) else [base]
364
+ if not any("Qwen/Qwen-Image" in b for b in bases):
365
+ raise ValueError("Not a Qwen-Image LoRA.")
366
+ trigger = card.data.get("instance_prompt", "")
367
+ img_path = card.data.get("widget", [{}])[0].get("output", {}).get("url")
368
+ image_url = f"https://huggingface.co/{repo}/resolve/main/{img_path}" if img_path else None
369
+ fs = HfFileSystem()
370
+ files = fs.ls(repo, detail=False)
371
+ weight = next((f.split("/")[-1] for f in files if f.endswith(".safetensors")), None)
372
+ if not weight:
373
+ raise ValueError("No .safetensors found.")
374
+ return parts[-1], repo, weight, trigger, image_url
375
+
376
+ def add_custom_lora(custom_text: str):
377
+ global loras
378
+ if not custom_text.strip():
379
+ return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None
380
+ try:
381
+ link = custom_text.strip()
382
+ if "huggingface.co/" in link:
383
+ link = link.split("huggingface.co/")[-1]
384
+ title, repo, weight, trigger, image = fetch_hf_lora(link)
385
+ existing = next((i for i, l in enumerate(loras) if l["repo"] == repo), None)
386
+ if existing is None:
387
+ loras.append({"image": image, "title": title, "repo": repo, "weights": weight, "trigger_word": trigger})
388
+ existing = len(loras) - 1
389
+ card_html = f"""
390
+ <div style="background:#1a1816;border:1px solid #272422;border-radius:8px;padding:10px;
391
+ display:flex;align-items:center;gap:10px;margin-top:6px">
392
+ {'<img src="'+image+'" style="width:48px;height:48px;object-fit:cover;border-radius:5px">' if image else ''}
393
+ <div>
394
+ <div style="font-size:13px;font-weight:600;color:#f0ebe5">{title}</div>
395
+ <div style="font-size:10px;font-family:monospace;color:#9a9088;margin-top:2px">
396
+ {('trigger: <b>'+trigger+'</b>') if trigger else 'no trigger word'}
397
+ </div>
398
+ </div>
399
+ </div>"""
400
+ return gr.update(visible=True, value=card_html), gr.update(visible=True), gr.Gallery(selected_index=None), f"Custom: {weight}", existing
401
+ except Exception as e:
402
+ gr.Warning(str(e))
403
+ return gr.update(visible=True, value=f"<span style='color:#e24b4a'>{e}</span>"), gr.update(visible=True), gr.update(), "", None
404
+
405
+ def remove_custom_lora():
406
+ return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None
407
+
408
+ generate.zerogpu = True
409
+
410
+ # ── CSS ───────────────────────────────────────────────────────────────────────
411
+
412
+ CSS = """
413
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
414
+
415
+ body, .gradio-container { background: #0b0a09 !important; font-family: 'Space Grotesk', sans-serif !important; }
416
+
417
+ /* ── HEADER ── */
418
+ #drex-header {
419
+ text-align: center;
420
+ padding: 20px 0 8px;
421
+ border-bottom: 1px solid #272422;
422
+ margin-bottom: 16px;
423
+ }
424
+ #drex-header .drex-logo {
425
+ display: inline-flex; align-items: center; gap: 12px;
426
+ }
427
+ #drex-header .drex-mark {
428
+ width: 38px; height: 38px; background: #c2410c; border-radius: 8px;
429
+ display: flex; align-items: center; justify-content: center;
430
+ }
431
+ #drex-header .drex-mark svg { width: 20px; height: 20px; fill: white; }
432
+ #drex-header h1 {
433
+ font-family: 'Space Grotesk', sans-serif !important;
434
+ font-size: 28px !important; font-weight: 700 !important;
435
+ color: #f0ebe5 !important; letter-spacing: -1px; margin: 0;
436
+ }
437
+ #drex-header .drex-sub {
438
+ font-family: 'JetBrains Mono', monospace;
439
+ font-size: 11px; color: #524e4a; margin-top: 3px;
440
+ }
441
+
442
+ /* ── THEME BAR ── */
443
+ #theme-bar { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 0 14px; }
444
+ .theme-dot {
445
+ width: 20px; height: 20px; border-radius: 50%; cursor: pointer;
446
+ border: 2px solid transparent; transition: transform .15s;
447
+ display: inline-block;
448
+ }
449
+ .theme-dot:hover { transform: scale(1.2); }
450
+
451
+ /* ── BLOCKS ── */
452
+ .gradio-container .block {
453
+ background: #131110 !important;
454
+ border: 1px solid #272422 !important;
455
+ border-radius: 10px !important;
456
+ }
457
+ label, .label-wrap span { font-family: 'JetBrains Mono', monospace !important; font-size: 10px !important; color: #524e4a !important; letter-spacing: 1px; text-transform: uppercase; }
458
+
459
+ /* ── GALLERY ── */
460
+ #lora-gallery .grid-wrap { height: 200px !important; }
461
+ #lora-gallery .thumbnail-item { border-radius: 6px !important; border: 1px solid #272422 !important; overflow: hidden; }
462
+ #lora-gallery .thumbnail-item.selected { border-color: #c2410c !important; box-shadow: 0 0 0 1px #c2410c !important; }
463
+
464
+ /* ── INPUTS ── */
465
+ textarea, input[type=text], input[type=number] {
466
+ background: #1a1816 !important; border: 1px solid #272422 !important;
467
+ border-radius: 6px !important; color: #f0ebe5 !important;
468
+ font-family: 'Space Grotesk', sans-serif !important;
469
+ }
470
+ textarea:focus, input:focus { border-color: #c2410c !important; }
471
+
472
+ /* ── BUTTONS ── */
473
+ button.primary { background: #c2410c !important; border: none !important; border-radius: 6px !important; font-family: 'Space Grotesk', sans-serif !important; font-weight: 600 !important; }
474
+ button.primary:hover { filter: brightness(1.1); }
475
+ button.secondary { background: #1a1816 !important; border: 1px solid #272422 !important; border-radius: 6px !important; color: #9a9088 !important; }
476
+ button.secondary:hover { border-color: #c2410c !important; color: #c2410c !important; }
477
+
478
+ /* ── SLIDERS ── */
479
+ input[type=range] { accent-color: #c2410c; }
480
+
481
+ /* ── TABS ── */
482
+ .tab-nav button { font-family: 'JetBrains Mono', monospace !important; font-size: 11px !important; color: #524e4a !important; background: transparent !important; border: none !important; border-bottom: 2px solid transparent !important; border-radius: 0 !important; }
483
+ .tab-nav button.selected { color: #c2410c !important; border-bottom-color: #c2410c !important; }
484
+
485
+ /* ── STATUS ── */
486
+ #status-bar { font-family: 'JetBrains Mono', monospace !important; font-size: 11px !important; background: #131110 !important; border: 1px solid #272422 !important; border-radius: 6px !important; padding: 7px 12px !important; color: #9a9088 !important; }
487
+
488
+ /* ── METADATA ── */
489
+ #meta-panel { font-family: 'JetBrains Mono', monospace !important; }
490
+
491
+ /* ── GEN BTN ── */
492
+ #gen-btn { height: 44px !important; font-size: 14px !important; letter-spacing: 0.3px; }
493
+
494
+ /* ── ACCORDION ── */
495
+ .accordion { background: #131110 !important; border: 1px solid #272422 !important; }
496
+ .accordion .label-wrap { color: #9a9088 !important; }
497
+ """
498
+
499
+ # ── JAVASCRIPT (theme switcher + prompt enhancer hint) ────────────────────────
500
+
501
+ JS_INIT = """
502
+ function() {
503
+ // expose accent-color CSS var for metadata panel
504
+ document.documentElement.style.setProperty('--drex-acc', '#c2410c');
505
+ }
506
+ """
507
+
508
+ # ── GRADIO UI ─────────────────────────────────────────────────────────────────
509
+
510
+ with gr.Blocks(title="D-REX Studio") as app:
511
+ selected_index = gr.State(value=None)
512
+ history_state = gr.State(value=[])
513
+
514
+ # ── HEADER ──
515
+ gr.HTML("""
516
+ <div id="drex-header">
517
+ <div class="drex-logo">
518
+ <div class="drex-mark">
519
+ <svg viewBox="0 0 16 16">
520
+ <path d="M8 0L1 4v8l7 4 7-4V4L8 0zm0 2.4L13 5.5 8 8.6 3 5.5 8 2.4z
521
+ M2.5 6.8l4.7 2.7v5.1l-4.7-2.7V6.8zm6.3 7.8V9.5l4.7-2.7v5.1l-4.7 2.7z"/>
522
+ </svg>
523
+ </div>
524
+ <div>
525
+ <h1>D-REX</h1>
526
+ <div class="drex-sub">LoRA Studio Β· Qwen-Image</div>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ """)
531
+
532
+ # ── LOGIN ──
533
+ with gr.Row():
534
+ gr.LoginButton(scale=0)
535
+
536
+ # ── THEME BAR ──
537
+ with gr.Row():
538
+ theme_selector = gr.HTML("""
539
+ <div id="theme-bar">
540
+ <span style="font-size:10px;font-family:monospace;color:#524e4a;margin-right:4px">THEME</span>
541
+ <span class="theme-dot" style="background:#c2410c" title="Orange Red"></span>
542
+ <span class="theme-dot" style="background:#7c3aed" title="Violet"></span>
543
+ <span class="theme-dot" style="background:#0369a1" title="Ocean"></span>
544
+ <span class="theme-dot" style="background:#047857" title="Emerald"></span>
545
+ <span class="theme-dot" style="background:#be185d" title="Rose"></span>
546
+ <span class="theme-dot" style="background:#b45309" title="Amber"></span>
547
+ <span style="font-size:10px;font-family:monospace;color:#524e4a;margin-left:8px">CUSTOM</span>
548
+ </div>
549
+ <div style="display:flex;justify-content:center;gap:8px;margin-bottom:12px">
550
+ <input id="custom-theme-hex" type="text" maxlength="7" placeholder="#hex color"
551
+ style="width:100px;background:#1a1816;border:1px solid #272422;border-radius:5px;
552
+ padding:4px 8px;font-size:11px;font-family:monospace;color:#f0ebe5;outline:none"/>
553
+ <button onclick="
554
+ const v=document.getElementById('custom-theme-hex').value.trim();
555
+ if(/^#[0-9a-fA-F]{6}$/.test(v)){
556
+ document.querySelectorAll('button.primary').forEach(b=>b.style.background=v);
557
+ document.querySelectorAll('input[type=range]').forEach(r=>r.style.accentColor=v);
558
+ document.documentElement.style.setProperty('--drex-acc',v);
559
+ }"
560
+ style="background:#1a1816;border:1px solid #272422;border-radius:5px;
561
+ padding:4px 12px;font-size:11px;font-family:monospace;color:#9a9088;cursor:pointer">
562
+ apply
563
+ </button>
564
+ </div>
565
+ """)
566
+
567
+ # ── MAIN ──
568
+ with gr.Row():
569
+
570
+ # ── LEFT PANEL ──
571
+ with gr.Column(scale=4):
572
+
573
+ with gr.Tabs():
574
+
575
+ with gr.Tab("Gallery"):
576
+ gallery = gr.Gallery(
577
+ value=[(l["image"], l["title"]) for l in loras],
578
+ label=None,
579
+ allow_preview=False,
580
+ columns=3,
581
+ elem_id="lora-gallery",
582
+ show_label=False,
583
+ )
584
+ selected_info = gr.Markdown("", elem_id="lora-info")
585
+
586
+ with gr.Tab("Favorites"):
587
+ gr.Markdown("*Star a LoRA in the gallery to save it here.*",
588
+ elem_id="fav-placeholder")
589
+ fav_html = gr.HTML(
590
+ "<div style='font-size:11px;font-family:monospace;color:#524e4a;padding:12px;text-align:center'>no favorites yet</div>"
591
+ )
592
+ gr.Markdown(
593
+ "> Tip: Right-click a LoRA image β†’ **Add to favorites** coming in next update.",
594
+ elem_id="fav-tip"
595
+ )
596
+
597
+ with gr.Tab("History"):
598
+ history_html = gr.HTML(
599
+ "<div style='font-size:11px;font-family:monospace;color:#524e4a;padding:12px;text-align:center'>no history yet</div>",
600
+ elem_id="history-display",
601
+ )
602
+ clear_history_btn = gr.Button("Clear history", size="sm", variant="secondary")
603
+
604
+ with gr.Tab("Custom LoRA"):
605
+ custom_lora_input = gr.Textbox(
606
+ label="HuggingFace repo",
607
+ placeholder="username/lora-model-name",
608
+ show_label=True,
609
+ )
610
+ gr.Markdown("[Browse Qwen-Image LoRAs β†’](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image)")
611
+ custom_lora_info = gr.HTML(visible=False)
612
+ custom_lora_remove = gr.Button("Remove custom LoRA", visible=False, size="sm", variant="secondary")
613
+
614
+ # ── RIGHT PANEL ──
615
+ with gr.Column(scale=5):
616
+
617
+ with gr.Row():
618
+ prompt = gr.Textbox(
619
+ label="Prompt",
620
+ placeholder="Describe your image...",
621
+ lines=2,
622
+ scale=5,
623
+ )
624
+ with gr.Column(scale=1, min_width=90):
625
+ enhance_btn = gr.Button("✦ Enhance", variant="secondary", size="sm")
626
+ gen_btn = gr.Button("Generate", variant="primary", elem_id="gen-btn")
627
+
628
+ neg_prompt = gr.Textbox(
629
+ label="Negative prompt",
630
+ placeholder="blur, watermark, low quality...",
631
+ lines=1,
632
+ )
633
+
634
+ result = gr.Image(label="Output", format="png", elem_id="output-image")
635
+
636
+ meta_panel = gr.HTML(
637
+ "<div></div>",
638
+ elem_id="meta-panel",
639
+ visible=True,
640
+ )
641
+
642
+ with gr.Row():
643
+ aspect_ratio = gr.Dropdown(
644
+ label="Aspect ratio",
645
+ choices=["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"],
646
+ value="3:2",
647
+ )
648
+ speed_mode = gr.Dropdown(
649
+ label="Mode",
650
+ choices=["Base Β· 50 steps", "Fast Β· 8 steps"],
651
+ value="Base Β· 50 steps",
652
+ )
653
+
654
+ status_bar = gr.Textbox(
655
+ value="D-REX ready Β· base Β· 50 steps",
656
+ label=None,
657
+ interactive=False,
658
+ show_label=False,
659
+ elem_id="status-bar",
660
+ )
661
+
662
+ with gr.Accordion("Advanced settings", open=False):
663
+ with gr.Row():
664
+ cfg_scale = gr.Slider(
665
+ label="CFG Scale", minimum=1.0, maximum=5.0, step=0.1, value=4.0
666
+ )
667
+ steps = gr.Slider(
668
+ label="Steps", minimum=4, maximum=50, step=1, value=50
669
+ )
670
+ with gr.Row():
671
+ randomize_seed = gr.Checkbox(True, label="Randomize seed")
672
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0, randomize=True)
673
+ lora_scale = gr.Slider(label="LoRA scale", minimum=0, maximum=2, step=0.01, value=1.0)
674
+
675
+ # ── EVENTS ───────────────────────────────────────────────────────────────
676
+
677
+ gallery.select(
678
+ on_lora_select,
679
+ inputs=[aspect_ratio],
680
+ outputs=[prompt, selected_info, selected_index, aspect_ratio],
681
+ )
682
+
683
+ speed_mode.change(
684
+ on_speed_change,
685
+ inputs=[speed_mode],
686
+ outputs=[status_bar, steps, cfg_scale],
687
+ )
688
+
689
+ enhance_btn.click(
690
+ enhance_prompt,
691
+ inputs=[prompt],
692
+ outputs=[prompt],
693
+ )
694
+
695
+ gr.on(
696
+ triggers=[gen_btn.click, prompt.submit],
697
+ fn=generate,
698
+ inputs=[
699
+ prompt, neg_prompt, cfg_scale, steps,
700
+ selected_index, randomize_seed, seed,
701
+ aspect_ratio, lora_scale, speed_mode,
702
+ history_state,
703
+ ],
704
+ outputs=[result, seed, meta_panel, history_html, history_state],
705
+ )
706
+ # Note: oauth_token injected automatically by Gradio from LoginButton
707
+
708
+ custom_lora_input.input(
709
+ add_custom_lora,
710
+ inputs=[custom_lora_input],
711
+ outputs=[custom_lora_info, custom_lora_remove, gallery, selected_info, selected_index],
712
+ )
713
+
714
+ custom_lora_remove.click(
715
+ remove_custom_lora,
716
+ outputs=[custom_lora_info, custom_lora_remove, gallery, selected_info, selected_index],
717
+ )
718
+
719
+ clear_history_btn.click(
720
+ lambda: ([], "<div style='font-size:11px;font-family:monospace;color:#524e4a;padding:12px;text-align:center'>history cleared</div>"),
721
+ outputs=[history_state, history_html],
722
+ )
723
+
724
+ # ── LAUNCH ────────────────────────────────────────────────────────────────────
725
+
726
+ app.queue()
727
+ app.launch(
728
+ theme=drex_theme,
729
+ css=CSS,
730
+ js=JS_INIT,
731
+ )