Spaces:
Running on Zero
Running on Zero
| import os | |
| import json | |
| import math | |
| import time | |
| import random | |
| import numpy as np | |
| from typing import Iterable | |
| import torch | |
| from PIL import Image | |
| import gradio as gr | |
| import spaces | |
| from diffusers import DiffusionPipeline, FlowMatchEulerDiscreteScheduler | |
| from huggingface_hub import HfFileSystem, ModelCard | |
| from gradio.themes import Soft | |
| from gradio.themes.utils import colors, fonts, sizes | |
| # ββ THEME ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| THEME_PRESETS = { | |
| "orange-red": {"primary": "#c2410c", "secondary": "#ea580c"}, | |
| "violet": {"primary": "#7c3aed", "secondary": "#8b5cf6"}, | |
| "ocean": {"primary": "#0369a1", "secondary": "#0ea5e9"}, | |
| "emerald": {"primary": "#226105", "secondary": "#73ff00"}, | |
| "rose": {"primary": "#be185d", "secondary": "#ec4899"}, | |
| "amber": {"primary": "#b45309", "secondary": "#f59e0b"}, | |
| "cyan": {"primary": "#0e7490", "secondary": "#06b6d4"}, | |
| } | |
| def make_color(name: str, hex_val: str) -> colors.Color: | |
| """Generate a Gradio Color object from a single hex value (simplified ramp).""" | |
| return colors.Color( | |
| name=name, | |
| c50=hex_val + "15", | |
| c100=hex_val + "25", | |
| c200=hex_val + "40", | |
| c300=hex_val + "60", | |
| c400=hex_val + "80", | |
| c500=hex_val, | |
| c600=hex_val, | |
| c700=hex_val, | |
| c800=hex_val, | |
| c900=hex_val, | |
| c950=hex_val, | |
| ) | |
| class DRexTheme(Soft): | |
| def __init__( | |
| self, | |
| primary_hue=colors.gray, | |
| accent_hex: str = "#c2410c", | |
| *, | |
| neutral_hue=colors.slate, | |
| text_size=sizes.text_md, | |
| font: Iterable[fonts.Font | str] = ( | |
| fonts.GoogleFont("Space Grotesk"), "Arial", "sans-serif" | |
| ), | |
| font_mono: Iterable[fonts.Font | str] = ( | |
| fonts.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace" | |
| ), | |
| ): | |
| super().__init__( | |
| primary_hue=primary_hue, | |
| neutral_hue=neutral_hue, | |
| text_size=text_size, | |
| font=font, | |
| font_mono=font_mono, | |
| ) | |
| a = accent_hex | |
| super().set( | |
| # backgrounds | |
| background_fill_primary="*primary_50", | |
| background_fill_primary_dark="*primary_900", | |
| body_background_fill="#0f0d0c", | |
| body_background_fill_dark="#0b0a09", | |
| # buttons | |
| button_primary_text_color="white", | |
| button_primary_text_color_hover="white", | |
| button_primary_background_fill=a, | |
| button_primary_background_fill_hover=a, | |
| button_primary_background_fill_dark=a, | |
| button_primary_background_fill_hover_dark=a, | |
| button_secondary_background_fill="*primary_200", | |
| button_secondary_background_fill_hover="*primary_300", | |
| # slider | |
| slider_color=a, | |
| slider_color_dark=a, | |
| # block | |
| block_title_text_weight="600", | |
| block_border_width="1px", | |
| block_shadow="none", | |
| block_label_background_fill="*primary_100", | |
| # input | |
| input_background_fill="#1a1816", | |
| input_background_fill_dark="#1a1816", | |
| input_border_color="#272422", | |
| input_border_color_dark="#272422", | |
| ) | |
| drex_theme = DRexTheme(accent_hex="#c2410c") | |
| # ββ GPU / DEVICE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| dtype = torch.bfloat16 | |
| print(f"[D-REX] Using device: {device}") | |
| # ββ LORAS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| loras = [ | |
| { | |
| "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Studio-Realism/resolve/main/images/2.png", | |
| "title": "Studio Realism", | |
| "repo": "prithivMLmods/Qwen-Image-Studio-Realism", | |
| "weights": "qwen-studio-realism.safetensors", | |
| "trigger_word": "Studio Realism", | |
| }, | |
| { | |
| "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Sketch-Smudge/resolve/main/images/1.png", | |
| "title": "Sketch Smudge", | |
| "repo": "prithivMLmods/Qwen-Image-Sketch-Smudge", | |
| "weights": "qwen-sketch-smudge.safetensors", | |
| "trigger_word": "Sketch Smudge", | |
| }, | |
| { | |
| "image": "https://huggingface.co/Shakker-Labs/AWPortrait-QW/resolve/main/images/08fdaf6b644b61136340d5c908ca37993e47f34cdbe2e8e8251c4c72.jpg", | |
| "title": "AWPortrait QW", | |
| "repo": "Shakker-Labs/AWPortrait-QW", | |
| "weights": "AWPortrait-QW_1.0.safetensors", | |
| "trigger_word": "Portrait", | |
| }, | |
| { | |
| "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Anime-LoRA/resolve/main/images/1.png", | |
| "title": "Qwen Anime", | |
| "repo": "prithivMLmods/Qwen-Image-Anime-LoRA", | |
| "weights": "qwen-anime.safetensors", | |
| "trigger_word": "Qwen Anime", | |
| }, | |
| { | |
| "image": "https://huggingface.co/flymy-ai/qwen-image-realism-lora/resolve/main/assets/flymy_realism.png", | |
| "title": "Image Realism", | |
| "repo": "flymy-ai/qwen-image-realism-lora", | |
| "weights": "flymy_realism.safetensors", | |
| "trigger_word": "Super Realism Portrait", | |
| }, | |
| { | |
| "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Fragmented-Portraiture/resolve/main/images/3.png", | |
| "title": "Fragmented Portraiture", | |
| "repo": "prithivMLmods/Qwen-Image-Fragmented-Portraiture", | |
| "weights": "qwen-fragmented-portraiture.safetensors", | |
| "trigger_word": "Fragmented Portraiture", | |
| }, | |
| { | |
| "image": "https://huggingface.co/prithivMLmods/Qwen-Image-Synthetic-Face/resolve/main/images/2.png", | |
| "title": "Synthetic Face", | |
| "repo": "prithivMLmods/Qwen-Image-Synthetic-Face", | |
| "weights": "qwen-synthetic-face.safetensors", | |
| "trigger_word": "Synthetic Face", | |
| }, | |
| { | |
| "image": "https://huggingface.co/itspoidaman/qwenglitch/resolve/main/images/GyZTwJIbkAAhS4h.jpeg", | |
| "title": "Qwen Glitch", | |
| "repo": "itspoidaman/qwenglitch", | |
| "weights": "qwenglitch1.safetensors", | |
| "trigger_word": "qwenglitch", | |
| }, | |
| { | |
| "image": "https://huggingface.co/alfredplpl/qwen-image-modern-anime-lora/resolve/main/sample1.jpg", | |
| "title": "Modern Anime", | |
| "repo": "alfredplpl/qwen-image-modern-anime-lora", | |
| "weights": "lora.safetensors", | |
| "trigger_word": "Japanese modern anime style", | |
| }, | |
| ] | |
| # ββ MODEL INIT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| scheduler_config = { | |
| "base_image_seq_len": 256, | |
| "base_shift": math.log(3), | |
| "invert_sigmas": False, | |
| "max_image_seq_len": 8192, | |
| "max_shift": math.log(3), | |
| "num_train_timesteps": 1000, | |
| "shift": 1.0, | |
| "shift_terminal": None, | |
| "stochastic_sampling": False, | |
| "time_shift_type": "exponential", | |
| "use_beta_sigmas": False, | |
| "use_dynamic_shifting": True, | |
| "use_exponential_sigmas": False, | |
| "use_karras_sigmas": False, | |
| } | |
| scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config) | |
| # ZeroGPU: load on CPU β moved to cuda inside @spaces.GPU function only | |
| pipe = DiffusionPipeline.from_pretrained( | |
| "Qwen/Qwen-Image", scheduler=scheduler, torch_dtype=dtype | |
| ) | |
| LIGHTNING_REPO = "lightx2v/Qwen-Image-Lightning" | |
| LIGHTNING_WEIGHT = "Qwen-Image-Lightning-8steps-V1.0.safetensors" | |
| MAX_SEED = np.iinfo(np.int32).max | |
| ENHANCE_SUFFIXES = [ | |
| ", ultra detailed, cinematic lighting, 8k resolution, professional photography", | |
| ", masterpiece, intricate details, dramatic composition, volumetric light", | |
| ", photorealistic, sharp focus, studio lighting, high contrast, award winning", | |
| ", concept art, highly detailed, trending on artstation, vivid colors, epic", | |
| ", hyperrealistic, golden hour lighting, bokeh, atmospheric depth, stunning", | |
| ] | |
| # ββ HELPERS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def aspect_to_wh(aspect: str): | |
| mapping = { | |
| "1:1": (1024, 1024), "16:9": (1152, 640), "9:16": (640, 1152), | |
| "4:3": (1024, 768), "3:4": (768, 1024), "3:2": (1024, 688), | |
| "2:3": (688, 1024), | |
| } | |
| return mapping.get(aspect, (1024, 1024)) | |
| def build_metadata_html(model_title: str, seed: int, steps: int, cfg: float, aspect: str) -> str: | |
| w, h = aspect_to_wh(aspect) | |
| return f""" | |
| <div style=" | |
| display:grid;grid-template-columns:repeat(5,1fr);gap:8px; | |
| background:#131110;border:1px solid #272422;border-radius:8px; | |
| padding:10px 14px;font-family:'JetBrains Mono',monospace;margin-top:6px"> | |
| <div style="text-align:center"> | |
| <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{model_title.split()[0]}</div> | |
| <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">MODEL</div> | |
| </div> | |
| <div style="text-align:center"> | |
| <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{seed}</div> | |
| <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">SEED</div> | |
| </div> | |
| <div style="text-align:center"> | |
| <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{steps}</div> | |
| <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">STEPS</div> | |
| </div> | |
| <div style="text-align:center"> | |
| <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{cfg:.1f}</div> | |
| <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">CFG</div> | |
| </div> | |
| <div style="text-align:center"> | |
| <div style="font-size:13px;font-weight:600;color:var(--drex-acc,#c2410c)">{w}Γ{h}</div> | |
| <div style="font-size:9px;color:#524e4a;letter-spacing:1px;margin-top:2px">SIZE</div> | |
| </div> | |
| </div>""" | |
| def format_history_html(history: list) -> str: | |
| if not history: | |
| return "<div style='font-size:11px;font-family:monospace;color:#524e4a;padding:12px;text-align:center'>no history yet</div>" | |
| rows = "" | |
| for h in reversed(history[-10:]): | |
| rows += f""" | |
| <div style="padding:6px 8px;background:#1a1816;border:1px solid #272422; | |
| border-radius:5px;margin-bottom:5px;cursor:pointer" | |
| onclick="document.querySelector('textarea').value='{h['prompt'].replace("'","")}'"> | |
| <div style="font-size:10px;font-family:monospace;color:#9a9088; | |
| overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{h['prompt']}</div> | |
| <div style="font-size:9px;font-family:monospace;color:#524e4a;margin-top:2px">{h['model']} Β· {h['time']}</div> | |
| </div>""" | |
| return f"<div style='max-height:180px;overflow-y:auto'>{rows}</div>" | |
| # ββ CORE LOGIC ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate( | |
| prompt, neg_prompt, cfg, steps, selected_index, | |
| randomize_seed, seed, aspect, lora_scale, speed_mode, | |
| history_state, | |
| oauth_token: gr.OAuthToken | None = None, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| if oauth_token is None: | |
| raise gr.Error("Please sign in with your HuggingFace account to generate images.") | |
| if selected_index is None: | |
| raise gr.Error("Select a LoRA from the gallery first.") | |
| if not prompt.strip(): | |
| raise gr.Error("Write a prompt before generating.") | |
| lora = loras[selected_index] | |
| trigger = lora["trigger_word"] | |
| prompt_in = f"{trigger} {prompt}" if trigger else prompt | |
| # ZeroGPU: LoRA loading must happen inside GPU scope | |
| pipe.to("cuda") | |
| try: | |
| pipe.unload_lora_weights() | |
| if speed_mode == "Fast Β· 8 steps": | |
| pipe.load_lora_weights(LIGHTNING_REPO, weight_name=LIGHTNING_WEIGHT, adapter_name="lightning") | |
| pipe.load_lora_weights(lora["repo"], weight_name=lora.get("weights"), low_cpu_mem_usage=True, adapter_name="style") | |
| pipe.set_adapters(["lightning", "style"], adapter_weights=[1.0, lora_scale]) | |
| else: | |
| pipe.load_lora_weights(lora["repo"], weight_name=lora.get("weights"), low_cpu_mem_usage=True, adapter_name="style") | |
| pipe.set_adapters(["style"], adapter_weights=[lora_scale]) | |
| if randomize_seed: | |
| seed = random.randint(0, MAX_SEED) | |
| w, h = aspect_to_wh(aspect) | |
| generator = torch.Generator(device="cuda").manual_seed(seed) | |
| image = pipe( | |
| prompt=prompt_in, | |
| negative_prompt=neg_prompt, | |
| num_inference_steps=steps, | |
| true_cfg_scale=cfg, | |
| width=w, | |
| height=h, | |
| generator=generator, | |
| ).images[0] | |
| finally: | |
| pipe.to("cpu") | |
| torch.cuda.empty_cache() | |
| # update history | |
| history_state = history_state or [] | |
| history_state.append({ | |
| "prompt": prompt[:80], | |
| "model": lora["title"], | |
| "time": time.strftime("%H:%M"), | |
| }) | |
| history_state = history_state[-20:] | |
| meta_html = build_metadata_html(lora["title"], seed, steps, cfg, aspect) | |
| history_html = format_history_html(history_state) | |
| return image, seed, meta_html, history_html, history_state | |
| def enhance_prompt(prompt: str) -> str: | |
| if not prompt.strip(): | |
| return prompt | |
| suffix = random.choice(ENHANCE_SUFFIXES) | |
| base = prompt.rstrip(".").rstrip(",").strip() | |
| return base + suffix | |
| def on_lora_select(evt: gr.SelectData, aspect): | |
| lora = loras[evt.index] | |
| placeholder = f"Describe your image for {lora['title']}..." | |
| info_md = f"### [{lora['repo']}](https://huggingface.co/{lora['repo']}) β " | |
| new_aspect = aspect | |
| if "aspect" in lora: | |
| new_aspect = {"portrait": "9:16", "landscape": "16:9"}.get(lora["aspect"], aspect) | |
| return gr.update(placeholder=placeholder), info_md, evt.index, new_aspect | |
| def on_speed_change(speed): | |
| if speed == "Fast Β· 8 steps": | |
| return gr.update(value="Fast Β· 8 steps with Lightning LoRA"), 8, 1.0 | |
| return gr.update(value="Base Β· 50 steps β best quality"), 50, 4.0 | |
| def fetch_hf_lora(link: str): | |
| parts = link.strip("/").split("/") | |
| if len(parts) < 2: | |
| raise ValueError("Invalid repo path.") | |
| repo = "/".join(parts[-2:]) | |
| card = ModelCard.load(repo) | |
| base = card.data.get("base_model", "") | |
| bases = base if isinstance(base, list) else [base] | |
| if not any("Qwen/Qwen-Image" in b for b in bases): | |
| raise ValueError("Not a Qwen-Image LoRA.") | |
| trigger = card.data.get("instance_prompt", "") | |
| img_path = card.data.get("widget", [{}])[0].get("output", {}).get("url") | |
| image_url = f"https://huggingface.co/{repo}/resolve/main/{img_path}" if img_path else None | |
| fs = HfFileSystem() | |
| files = fs.ls(repo, detail=False) | |
| weight = next((f.split("/")[-1] for f in files if f.endswith(".safetensors")), None) | |
| if not weight: | |
| raise ValueError("No .safetensors found.") | |
| return parts[-1], repo, weight, trigger, image_url | |
| def add_custom_lora(custom_text: str): | |
| global loras | |
| if not custom_text.strip(): | |
| return gr.update(visible=False), gr.update(visible=False), gr.update(), "", None | |
| try: | |
| link = custom_text.strip() | |
| if "huggingface.co/" in link: | |
| link = link.split("huggingface.co/")[-1] | |
| title, repo, weight, trigger, image = fetch_hf_lora(link) | |
| existing = next((i for i, l in enumerate(loras) if l["repo"] == repo), None) | |
| if existing is None: | |
| loras.append({"image": image, "title": title, "repo": repo, "weights": weight, "trigger_word": trigger}) | |
| existing = len(loras) - 1 | |
| card_html = f""" | |
| <div style="background:#1a1816;border:1px solid #272422;border-radius:8px;padding:10px; | |
| display:flex;align-items:center;gap:10px;margin-top:6px"> | |
| {'<img src="'+image+'" style="width:48px;height:48px;object-fit:cover;border-radius:5px">' if image else ''} | |
| <div> | |
| <div style="font-size:13px;font-weight:600;color:#f0ebe5">{title}</div> | |
| <div style="font-size:10px;font-family:monospace;color:#9a9088;margin-top:2px"> | |
| {('trigger: <b>'+trigger+'</b>') if trigger else 'no trigger word'} | |
| </div> | |
| </div> | |
| </div>""" | |
| new_gallery = [(l["image"], l["title"]) for l in loras] | |
| return (gr.update(visible=True, value=card_html), gr.update(visible=True), | |
| gr.update(value=new_gallery, selected_index=None), f"Custom: {weight}", existing) | |
| except Exception as e: | |
| gr.Warning(str(e)) | |
| return gr.update(visible=True, value=f"<span style='color:#e24b4a'>{e}</span>"), gr.update(visible=True), gr.update(), "", None | |
| def remove_custom_lora(): | |
| gallery_reset = [(l["image"], l["title"]) for l in loras] | |
| return gr.update(visible=False), gr.update(visible=False), gr.update(value=gallery_reset), "", None | |
| generate.zerogpu = True | |
| # ββ CSS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); | |
| body, .gradio-container { background: #0b0a09 !important; font-family: 'Space Grotesk', sans-serif !important; } | |
| /* ββ HEADER ββ */ | |
| #drex-header { | |
| text-align: center; | |
| padding: 20px 0 8px; | |
| border-bottom: 1px solid #272422; | |
| margin-bottom: 16px; | |
| } | |
| #drex-header .drex-logo { | |
| display: inline-flex; align-items: center; gap: 12px; | |
| } | |
| #drex-header .drex-mark { | |
| width: 38px; height: 38px; background: #c2410c; border-radius: 8px; | |
| display: flex; align-items: center; justify-content: center; | |
| } | |
| #drex-header .drex-mark svg { width: 20px; height: 20px; fill: white; } | |
| #drex-header h1 { | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| font-size: 28px !important; font-weight: 700 !important; | |
| color: #f0ebe5 !important; letter-spacing: -1px; margin: 0; | |
| } | |
| #drex-header .drex-sub { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; color: #524e4a; margin-top: 3px; | |
| } | |
| /* ββ THEME BAR ββ */ | |
| #theme-bar { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 0 14px; } | |
| .theme-dot { | |
| width: 20px; height: 20px; border-radius: 50%; cursor: pointer; | |
| border: 2px solid transparent; transition: transform .15s; | |
| display: inline-block; | |
| } | |
| .theme-dot:hover { transform: scale(1.2); } | |
| /* ββ BLOCKS ββ */ | |
| .gradio-container .block { | |
| background: #131110 !important; | |
| border: 1px solid #272422 !important; | |
| border-radius: 10px !important; | |
| } | |
| label, .label-wrap span { font-family: 'JetBrains Mono', monospace !important; font-size: 10px !important; color: #524e4a !important; letter-spacing: 1px; text-transform: uppercase; } | |
| /* ββ GALLERY ββ */ | |
| #lora-gallery .grid-wrap { height: 200px !important; } | |
| #lora-gallery .thumbnail-item { border-radius: 6px !important; border: 1px solid #272422 !important; overflow: hidden; } | |
| #lora-gallery .thumbnail-item.selected { border-color: #c2410c !important; box-shadow: 0 0 0 1px #c2410c !important; } | |
| /* ββ INPUTS ββ */ | |
| textarea, input[type=text], input[type=number] { | |
| background: #1a1816 !important; border: 1px solid #272422 !important; | |
| border-radius: 6px !important; color: #f0ebe5 !important; | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| } | |
| textarea:focus, input:focus { border-color: #c2410c !important; } | |
| /* ββ BUTTONS ββ */ | |
| button.primary { background: #c2410c !important; border: none !important; border-radius: 6px !important; font-family: 'Space Grotesk', sans-serif !important; font-weight: 600 !important; } | |
| button.primary:hover { filter: brightness(1.1); } | |
| button.secondary { background: #1a1816 !important; border: 1px solid #272422 !important; border-radius: 6px !important; color: #9a9088 !important; } | |
| button.secondary:hover { border-color: #c2410c !important; color: #c2410c !important; } | |
| /* ββ SLIDERS ββ */ | |
| input[type=range] { accent-color: #c2410c; } | |
| /* ββ TABS ββ */ | |
| .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; } | |
| .tab-nav button.selected { color: #c2410c !important; border-bottom-color: #c2410c !important; } | |
| /* ββ STATUS ββ */ | |
| #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; } | |
| /* ββ METADATA ββ */ | |
| #meta-panel { font-family: 'JetBrains Mono', monospace !important; } | |
| /* ββ GEN BTN ββ */ | |
| #gen-btn { height: 44px !important; font-size: 14px !important; letter-spacing: 0.3px; } | |
| /* ββ ACCORDION ββ */ | |
| .accordion { background: #131110 !important; border: 1px solid #272422 !important; } | |
| .accordion .label-wrap { color: #9a9088 !important; } | |
| """ | |
| # ββ JAVASCRIPT (theme switcher + prompt enhancer hint) ββββββββββββββββββββββββ | |
| JS_INIT = """ | |
| function() { | |
| // expose accent-color CSS var for metadata panel | |
| document.documentElement.style.setProperty('--drex-acc', '#c2410c'); | |
| } | |
| """ | |
| # ββ GRADIO UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(title="D-REX Studio") as app: | |
| selected_index = gr.State(value=None) | |
| history_state = gr.State(value=[]) | |
| # ββ HEADER ββ | |
| gr.HTML(""" | |
| <div id="drex-header"> | |
| <div class="drex-logo"> | |
| <div class="drex-mark"> | |
| <svg viewBox="0 0 16 16"> | |
| <path d="M8 0L1 4v8l7 4 7-4V4L8 0zm0 2.4L13 5.5 8 8.6 3 5.5 8 2.4z | |
| 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"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <h1>D-REX</h1> | |
| <div class="drex-sub">LoRA Studio Β· Qwen-Image</div> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # ββ LOGIN ββ | |
| with gr.Row(): | |
| gr.LoginButton(scale=0) | |
| # ββ THEME BAR ββ | |
| with gr.Row(): | |
| theme_selector = gr.HTML(""" | |
| <div id="theme-bar"> | |
| <span style="font-size:10px;font-family:monospace;color:#524e4a;margin-right:4px">THEME</span> | |
| <span class="theme-dot" style="background:#c2410c" title="Orange Red"></span> | |
| <span class="theme-dot" style="background:#7c3aed" title="Violet"></span> | |
| <span class="theme-dot" style="background:#0369a1" title="Ocean"></span> | |
| <span class="theme-dot" style="background:#047857" title="Emerald"></span> | |
| <span class="theme-dot" style="background:#be185d" title="Rose"></span> | |
| <span class="theme-dot" style="background:#b45309" title="Amber"></span> | |
| <span style="font-size:10px;font-family:monospace;color:#524e4a;margin-left:8px">CUSTOM</span> | |
| </div> | |
| <div style="display:flex;justify-content:center;gap:8px;margin-bottom:12px"> | |
| <input id="custom-theme-hex" type="text" maxlength="7" placeholder="#hex color" | |
| style="width:100px;background:#1a1816;border:1px solid #272422;border-radius:5px; | |
| padding:4px 8px;font-size:11px;font-family:monospace;color:#f0ebe5;outline:none"/> | |
| <button onclick=" | |
| const v=document.getElementById('custom-theme-hex').value.trim(); | |
| if(/^#[0-9a-fA-F]{6}$/.test(v)){ | |
| document.querySelectorAll('button.primary').forEach(b=>b.style.background=v); | |
| document.querySelectorAll('input[type=range]').forEach(r=>r.style.accentColor=v); | |
| document.documentElement.style.setProperty('--drex-acc',v); | |
| }" | |
| style="background:#1a1816;border:1px solid #272422;border-radius:5px; | |
| padding:4px 12px;font-size:11px;font-family:monospace;color:#9a9088;cursor:pointer"> | |
| apply | |
| </button> | |
| </div> | |
| """) | |
| # ββ MAIN ββ | |
| with gr.Row(): | |
| # ββ LEFT PANEL ββ | |
| with gr.Column(scale=4): | |
| with gr.Tabs(): | |
| with gr.Tab("Gallery"): | |
| gallery = gr.Gallery( | |
| value=[(l["image"], l["title"]) for l in loras], | |
| label=None, | |
| allow_preview=False, | |
| columns=3, | |
| elem_id="lora-gallery", | |
| show_label=False, | |
| ) | |
| selected_info = gr.Markdown("", elem_id="lora-info") | |
| with gr.Tab("Favorites"): | |
| gr.Markdown("*Star a LoRA in the gallery to save it here.*", | |
| elem_id="fav-placeholder") | |
| fav_html = gr.HTML( | |
| "<div style='font-size:11px;font-family:monospace;color:#524e4a;padding:12px;text-align:center'>no favorites yet</div>" | |
| ) | |
| gr.Markdown( | |
| "> Tip: Right-click a LoRA image β **Add to favorites** coming in next update.", | |
| elem_id="fav-tip" | |
| ) | |
| with gr.Tab("History"): | |
| history_html = gr.HTML( | |
| "<div style='font-size:11px;font-family:monospace;color:#524e4a;padding:12px;text-align:center'>no history yet</div>", | |
| elem_id="history-display", | |
| ) | |
| clear_history_btn = gr.Button("Clear history", size="sm", variant="secondary") | |
| with gr.Tab("Custom LoRA"): | |
| custom_lora_input = gr.Textbox( | |
| label="HuggingFace repo", | |
| placeholder="username/lora-model-name", | |
| show_label=True, | |
| ) | |
| gr.Markdown("[Browse Qwen-Image LoRAs β](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image)") | |
| custom_lora_info = gr.HTML(visible=False) | |
| custom_lora_remove = gr.Button("Remove custom LoRA", visible=False, size="sm", variant="secondary") | |
| # ββ RIGHT PANEL ββ | |
| with gr.Column(scale=5): | |
| with gr.Row(): | |
| prompt = gr.Textbox( | |
| label="Prompt", | |
| placeholder="Describe your image...", | |
| lines=2, | |
| scale=5, | |
| ) | |
| with gr.Column(scale=1, min_width=90): | |
| enhance_btn = gr.Button("β¦ Enhance", variant="secondary", size="sm") | |
| gen_btn = gr.Button("Generate", variant="primary", elem_id="gen-btn") | |
| neg_prompt = gr.Textbox( | |
| label="Negative prompt", | |
| placeholder="blur, watermark, low quality...", | |
| lines=1, | |
| ) | |
| result = gr.Image(label="Output", format="png", elem_id="output-image") | |
| meta_panel = gr.HTML( | |
| "<div></div>", | |
| elem_id="meta-panel", | |
| visible=True, | |
| ) | |
| with gr.Row(): | |
| aspect_ratio = gr.Dropdown( | |
| label="Aspect ratio", | |
| choices=["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"], | |
| value="3:2", | |
| ) | |
| speed_mode = gr.Dropdown( | |
| label="Mode", | |
| choices=["Base Β· 50 steps", "Fast Β· 8 steps"], | |
| value="Base Β· 50 steps", | |
| ) | |
| status_bar = gr.Textbox( | |
| value="D-REX ready Β· base Β· 50 steps", | |
| label=None, | |
| interactive=False, | |
| show_label=False, | |
| elem_id="status-bar", | |
| ) | |
| with gr.Accordion("Advanced settings", open=False): | |
| with gr.Row(): | |
| cfg_scale = gr.Slider( | |
| label="CFG Scale", minimum=1.0, maximum=5.0, step=0.1, value=4.0 | |
| ) | |
| steps = gr.Slider( | |
| label="Steps", minimum=4, maximum=50, step=1, value=50 | |
| ) | |
| with gr.Row(): | |
| randomize_seed = gr.Checkbox(True, label="Randomize seed") | |
| seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0, randomize=True) | |
| lora_scale = gr.Slider(label="LoRA scale", minimum=0, maximum=2, step=0.01, value=1.0) | |
| # ββ EVENTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gallery.select( | |
| on_lora_select, | |
| inputs=[aspect_ratio], | |
| outputs=[prompt, selected_info, selected_index, aspect_ratio], | |
| ) | |
| speed_mode.change( | |
| on_speed_change, | |
| inputs=[speed_mode], | |
| outputs=[status_bar, steps, cfg_scale], | |
| ) | |
| enhance_btn.click( | |
| enhance_prompt, | |
| inputs=[prompt], | |
| outputs=[prompt], | |
| ) | |
| gr.on( | |
| triggers=[gen_btn.click, prompt.submit], | |
| fn=generate, | |
| inputs=[ | |
| prompt, neg_prompt, cfg_scale, steps, | |
| selected_index, randomize_seed, seed, | |
| aspect_ratio, lora_scale, speed_mode, | |
| history_state, | |
| ], | |
| outputs=[result, seed, meta_panel, history_html, history_state], | |
| ) | |
| # Note: oauth_token injected automatically by Gradio from LoginButton | |
| custom_lora_input.input( | |
| add_custom_lora, | |
| inputs=[custom_lora_input], | |
| outputs=[custom_lora_info, custom_lora_remove, gallery, selected_info, selected_index], | |
| ) | |
| custom_lora_remove.click( | |
| remove_custom_lora, | |
| outputs=[custom_lora_info, custom_lora_remove, gallery, selected_info, selected_index], | |
| ) | |
| clear_history_btn.click( | |
| lambda: ([], "<div style='font-size:11px;font-family:monospace;color:#524e4a;padding:12px;text-align:center'>history cleared</div>"), | |
| outputs=[history_state, history_html], | |
| ) | |
| # ββ LAUNCH ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.queue() | |
| app.launch( | |
| theme=drex_theme, | |
| css=CSS, | |
| js=JS_INIT, | |
| ) |