Vector857 commited on
Commit
8edc85d
Β·
verified Β·
1 Parent(s): 0b9ad2d

Upload 2 files

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