artificialguybr's picture
Fix: Resolve ValueError and GatedRepoError issues
a3cce32
import hashlib
import inspect
import json
import os
from pathlib import Path
from typing import Any
import gradio as gr
import spaces
import torch
from diffusers import (
DEISMultistepScheduler,
DPMSolverMultistepScheduler,
DPMSolverSinglestepScheduler,
DiffusionPipeline,
EulerAncestralDiscreteScheduler,
EulerDiscreteScheduler,
HeunDiscreteScheduler,
KDPM2AncestralDiscreteScheduler,
KDPM2DiscreteScheduler,
LMSDiscreteScheduler,
UniPCMultistepScheduler,
)
from PIL import Image, ImageColor, ImageDraw, ImageFont
CATALOG_PATH = Path("loras.json")
COVER_CACHE_DIR = Path("images/auto-covers")
COVER_CACHE_DIR.mkdir(parents=True, exist_ok=True)
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
HF_TOKEN = os.environ.get("HF_TOKEN")
DEFAULT_NEGATIVE = (
"low quality, bad anatomy, bad hands, text, watermark, blurry, jpeg artifacts"
)
FAMILY_LABELS = {
"sdxl": "SDXL",
"sd15": "SD 1.5",
"flux": "FLUX",
"qwen-image": "Qwen-Image",
"z-image": "Z-Image",
"other": "Other",
"all": "All",
}
FAMILY_BASE_MODELS = {
"sdxl": "stabilityai/stable-diffusion-xl-base-1.0",
"sd15": "runwayml/stable-diffusion-v1-5",
"flux": "black-forest-labs/FLUX.2-klein-9B",
"qwen-image": "Qwen/Qwen-Image-2512",
"z-image": "Tongyi-MAI/Z-Image-Turbo",
}
FAMILY_DEFAULTS = {
"sdxl": {"steps": 30, "cfg": 7.0, "width": 1024, "height": 1024},
"sd15": {"steps": 28, "cfg": 7.5, "width": 768, "height": 768},
"flux": {"steps": 30, "cfg": 3.5, "width": 1024, "height": 1024},
"qwen-image": {"steps": 28, "cfg": 4.0, "width": 1024, "height": 1024},
"z-image": {"steps": 28, "cfg": 4.0, "width": 1024, "height": 1024},
"other": {"steps": 30, "cfg": 6.0, "width": 1024, "height": 1024},
}
SCHEDULER_CHOICES = [
"Auto",
"DPM++ 2M",
"DPM++ 2M Karras",
"DPM++ 2M SDE",
"DPM++ 2M SDE Karras",
"DPM++ SDE",
"DPM++ SDE Karras",
"DPM2",
"DPM2 Karras",
"DPM2 a",
"DPM2 a Karras",
"Euler",
"Euler a",
"Heun",
"LMS",
"LMS Karras",
"DEIS",
"UniPC",
]
SCHEDULER_MAP = {
"DPM++ 2M": lambda cfg: DPMSolverMultistepScheduler.from_config(cfg),
"DPM++ 2M Karras": lambda cfg: DPMSolverMultistepScheduler.from_config(cfg, use_karras_sigmas=True),
"DPM++ 2M SDE": lambda cfg: DPMSolverMultistepScheduler.from_config(cfg, algorithm_type="sde-dpmsolver++"),
"DPM++ 2M SDE Karras": lambda cfg: DPMSolverMultistepScheduler.from_config(cfg, use_karras_sigmas=True, algorithm_type="sde-dpmsolver++"),
"DPM++ SDE": lambda cfg: DPMSolverSinglestepScheduler.from_config(cfg),
"DPM++ SDE Karras": lambda cfg: DPMSolverSinglestepScheduler.from_config(cfg, use_karras_sigmas=True),
"DPM2": lambda cfg: KDPM2DiscreteScheduler.from_config(cfg),
"DPM2 Karras": lambda cfg: KDPM2DiscreteScheduler.from_config(cfg, use_karras_sigmas=True),
"DPM2 a": lambda cfg: KDPM2AncestralDiscreteScheduler.from_config(cfg),
"DPM2 a Karras": lambda cfg: KDPM2AncestralDiscreteScheduler.from_config(cfg, use_karras_sigmas=True),
"Euler": lambda cfg: EulerDiscreteScheduler.from_config(cfg),
"Euler a": lambda cfg: EulerAncestralDiscreteScheduler.from_config(cfg),
"Heun": lambda cfg: HeunDiscreteScheduler.from_config(cfg),
"LMS": lambda cfg: LMSDiscreteScheduler.from_config(cfg),
"LMS Karras": lambda cfg: LMSDiscreteScheduler.from_config(cfg, use_karras_sigmas=True),
"DEIS": lambda cfg: DEISMultistepScheduler.from_config(cfg),
"UniPC": lambda cfg: UniPCMultistepScheduler.from_config(cfg),
}
def load_lora_catalog() -> list[dict[str, Any]]:
if not CATALOG_PATH.exists():
raise RuntimeError("loras.json not found. Run scripts/update_loras_catalog.py first.")
with CATALOG_PATH.open("r", encoding="utf-8") as file:
catalog = json.load(file)
return catalog
LORAS = load_lora_catalog()
LORA_BY_REPO = {entry["repo"]: entry for entry in LORAS}
def family_to_label(family: str) -> str:
return FAMILY_LABELS.get(family, family.upper())
def fallback_cover_path(entry: dict[str, Any]) -> str:
key = f"{entry['repo']}::{entry.get('family', 'other')}"
digest = hashlib.sha256(key.encode("utf-8")).hexdigest()[:16]
output_path = COVER_CACHE_DIR / f"{digest}.png"
if output_path.exists():
return str(output_path)
family = entry.get("family", "other")
title = entry.get("title", "LoRA")
base_colors = {
"sdxl": ("#1d4ed8", "#38bdf8"),
"sd15": ("#0f766e", "#2dd4bf"),
"flux": ("#a21caf", "#f97316"),
"qwen-image": ("#3f3f46", "#60a5fa"),
"z-image": ("#065f46", "#22c55e"),
"other": ("#1f2937", "#9ca3af"),
}
color_a, color_b = base_colors.get(family, base_colors["other"])
width, height = 1024, 1024
image = Image.new("RGB", (width, height), color_a)
draw = ImageDraw.Draw(image)
for y in range(height):
alpha = y / max(1, height - 1)
r1, g1, b1 = ImageColor.getrgb(color_a)
r2, g2, b2 = ImageColor.getrgb(color_b)
color = (
int(r1 * (1 - alpha) + r2 * alpha),
int(g1 * (1 - alpha) + g2 * alpha),
int(b1 * (1 - alpha) + b2 * alpha),
)
draw.line([(0, y), (width, y)], fill=color)
font_title = ImageFont.load_default()
font_sub = ImageFont.load_default()
draw.text((60, 120), title[:60], fill="white", font=font_title)
draw.text((60, 180), family_to_label(family), fill="white", font=font_sub)
draw.text((60, 860), "artificialguybr", fill="white", font=font_sub)
image.save(output_path, format="PNG")
return str(output_path)
def cover_for_entry(entry: dict[str, Any]) -> str:
image_url = (entry.get("image") or "").strip()
return image_url if image_url else fallback_cover_path(entry)
def filter_loras(family: str, search: str) -> list[dict[str, Any]]:
term = (search or "").strip().lower()
filtered: list[dict[str, Any]] = []
for row in LORAS:
if family != "all" and row.get("family") != family:
continue
haystack = f"{row.get('title', '')} {row.get('repo', '')} {row.get('family', '')}".lower()
if term and term not in haystack:
continue
filtered.append(row)
filtered.sort(key=lambda item: item.get("title", "").lower())
return filtered
def family_defaults(family: str) -> dict[str, Any]:
return FAMILY_DEFAULTS.get(family, FAMILY_DEFAULTS["other"])
def scheduler_interactive_for_family(family: str) -> bool:
return family in {"sdxl", "sd15"}
def lora_dropdown_label(entry: dict[str, Any]) -> str:
short_repo = entry.get("repo", "").split("/", 1)[-1]
trigger = (entry.get("trigger_word") or "").strip()
if trigger:
return f"{entry['title']} · {short_repo} · trigger: {trigger}"
return f"{entry['title']} · {short_repo}"
def selected_payload(selected_repo: str):
if not selected_repo:
return (
gr.update(value=None),
gr.update(value=""),
gr.update(placeholder="Select a LoRA first"),
gr.update(),
gr.update(),
gr.update(),
gr.update(),
gr.update(),
gr.update(value="Auto", interactive=False),
gr.update(value=1.0),
)
selected = LORA_BY_REPO[selected_repo]
family = selected.get("family", "other")
defaults = family_defaults(family)
base_model = selected.get("base_model") or FAMILY_BASE_MODELS.get(family, "")
trigger = (selected.get("trigger_word") or "").strip()
placeholder = f"Describe your image for {selected['title']}"
if trigger:
placeholder += f" — trigger word: «{trigger}»"
badge_family = family_to_label(family)
trigger_line = f"🔑 Trigger: `{trigger}`" if trigger else "🔑 No trigger word"
info = (
f"#### {selected['title']}\n"
f"**`{selected_repo}`** · {badge_family}\n\n"
f"{trigger_line} \n"
f"Base: `{base_model or 'Unknown'}` \n"
f"Weight: `{selected.get('weight_name') or 'auto'}`"
)
scheduler_enabled = scheduler_interactive_for_family(family)
scheduler_value = "DPM++ 2M SDE Karras" if scheduler_enabled else "Auto"
return (
gr.update(value=cover_for_entry(selected)),
info,
gr.update(placeholder=placeholder),
selected_repo,
gr.update(value=defaults["steps"]),
gr.update(value=defaults["cfg"]),
gr.update(value=defaults["width"]),
gr.update(value=defaults["height"]),
gr.update(value=scheduler_value, interactive=scheduler_enabled),
gr.update(value=1.0),
)
def build_gallery_items(filtered: list[dict[str, Any]]) -> list[tuple[str, str]]:
return [(cover_for_entry(item), item["repo"]) for item in filtered]
def refresh_lora_selector(family: str, search: str):
filtered = filter_loras(family, search)
first_repo = filtered[0]["repo"] if filtered else None
count_text = f"**{len(filtered)}** LoRAs · {family_to_label(family)}"
gallery_items = build_gallery_items(filtered)
(
preview_update,
info_update,
prompt_update,
selected_repo_value,
steps_update,
cfg_update,
width_update,
height_update,
scheduler_update,
lora_scale_update,
) = selected_payload(first_repo)
return (
gr.update(value=gallery_items),
count_text,
preview_update,
info_update,
prompt_update,
selected_repo_value,
steps_update,
cfg_update,
width_update,
height_update,
scheduler_update,
lora_scale_update,
)
def on_gallery_select(evt: gr.SelectData, family: str, search: str):
filtered = filter_loras(family, search)
if evt.index >= len(filtered):
return (gr.update(),) * 10
repo = filtered[evt.index]["repo"]
return selected_payload(repo)
CURRENT_PIPE: DiffusionPipeline | None = None
CURRENT_BASE_MODEL = ""
CURRENT_LOADED_REPO = ""
def pick_dtype_for_family(family: str) -> torch.dtype:
if DEVICE != "cuda":
return torch.float32
if family in {"flux", "qwen-image", "z-image"}:
return torch.bfloat16
return torch.float16
def load_pipeline(base_model: str, family: str) -> DiffusionPipeline:
global CURRENT_PIPE, CURRENT_BASE_MODEL, CURRENT_LOADED_REPO
if CURRENT_PIPE is not None and CURRENT_BASE_MODEL == base_model:
return CURRENT_PIPE
if CURRENT_PIPE is not None:
del CURRENT_PIPE
CURRENT_PIPE = None
CURRENT_BASE_MODEL = ""
CURRENT_LOADED_REPO = ""
if torch.cuda.is_available():
torch.cuda.empty_cache()
dtype = pick_dtype_for_family(family)
pipe = DiffusionPipeline.from_pretrained(
base_model,
torch_dtype=dtype,
token=HF_TOKEN,
)
if DEVICE == "cuda":
pipe = pipe.to("cuda")
if hasattr(pipe, "enable_attention_slicing"):
pipe.enable_attention_slicing()
if hasattr(pipe, "enable_vae_slicing"):
pipe.enable_vae_slicing()
CURRENT_PIPE = pipe
CURRENT_BASE_MODEL = base_model
return pipe
def apply_scheduler(pipe: DiffusionPipeline, scheduler_name: str, family: str) -> None:
if scheduler_name == "Auto":
return
if family not in {"sdxl", "sd15"}:
return
if not hasattr(pipe, "scheduler"):
return
try:
scheduler_builder = SCHEDULER_MAP.get(scheduler_name)
if scheduler_builder:
pipe.scheduler = scheduler_builder(pipe.scheduler.config)
except Exception:
pass
def build_prompt(prompt: str, trigger_word: str) -> str:
prompt = (prompt or "").strip()
trigger_word = (trigger_word or "").strip()
if not prompt and not trigger_word:
return ""
if prompt and trigger_word:
return f"{prompt}, {trigger_word}"
return prompt or trigger_word
def round_dim(value: float | int) -> int:
dim = int(value)
return max(256, (dim // 8) * 8)
@spaces.GPU
def run_lora(
prompt: str,
negative_prompt: str,
cfg_scale: float,
steps: int,
selected_repo: str,
scheduler_name: str,
seed: int,
width: int,
height: int,
lora_scale: float,
):
global CURRENT_LOADED_REPO
if not selected_repo:
raise gr.Error("Select a LoRA from the list before generating.")
selected = LORA_BY_REPO.get(selected_repo)
if not selected:
raise gr.Error("Selected LoRA is not in the loaded catalog.")
family = selected.get("family", "other")
base_model = selected.get("base_model") or FAMILY_BASE_MODELS.get(family)
if not base_model:
raise gr.Error(f"No base model configured for {selected_repo}.")
full_prompt = build_prompt(prompt, selected.get("trigger_word", ""))
if not full_prompt:
raise gr.Error("Prompt cannot be empty.")
pipe = load_pipeline(base_model, family)
apply_scheduler(pipe, scheduler_name, family)
if CURRENT_LOADED_REPO and CURRENT_LOADED_REPO != selected_repo:
try:
pipe.unload_lora_weights()
except Exception:
pass
load_kwargs = {}
weight_name = (selected.get("weight_name") or "").strip()
if weight_name:
load_kwargs["weight_name"] = weight_name
if HF_TOKEN:
load_kwargs["token"] = HF_TOKEN
pipe.load_lora_weights(selected_repo, **load_kwargs)
CURRENT_LOADED_REPO = selected_repo
params = inspect.signature(pipe.__call__).parameters
kwargs: dict[str, Any] = {"prompt": full_prompt}
if "num_inference_steps" in params:
kwargs["num_inference_steps"] = int(steps)
if "guidance_scale" in params:
kwargs["guidance_scale"] = float(cfg_scale)
if "width" in params:
kwargs["width"] = round_dim(width)
if "height" in params:
kwargs["height"] = round_dim(height)
if "negative_prompt" in params and negative_prompt:
kwargs["negative_prompt"] = negative_prompt
elif "negative_prompt" in params and family in {"sdxl", "sd15"}:
kwargs["negative_prompt"] = DEFAULT_NEGATIVE
if "cross_attention_kwargs" in params:
kwargs["cross_attention_kwargs"] = {"scale": float(lora_scale)}
elif "joint_attention_kwargs" in params:
kwargs["joint_attention_kwargs"] = {"scale": float(lora_scale)}
elif "lora_scale" in params:
kwargs["lora_scale"] = float(lora_scale)
generator_device = "cuda" if DEVICE == "cuda" else "cpu"
kwargs["generator"] = torch.Generator(device=generator_device).manual_seed(int(seed))
result = pipe(**kwargs)
return result.images[0]
CSS = """
/* ── Google Fonts ── */
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@300;400;500&display=swap');
/* ── Reset & root ── */
:root {
--bg: #0a0a0f;
--surface: #111118;
--card: #16161f;
--border: #2a2a3a;
--accent: #7c6fff;
--accent2: #ff6fd8;
--accent3: #6fffd4;
--text: #e8e8f0;
--muted: #6b6b80;
--radius: 14px;
--radius-sm: 8px;
--shadow: 0 8px 32px rgba(0,0,0,0.5);
}
* { box-sizing: border-box; }
body, .gradio-container {
background: var(--bg) !important;
font-family: 'DM Sans', sans-serif !important;
color: var(--text) !important;
min-height: 100vh;
}
/* subtle grid background */
.gradio-container::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(124,111,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(124,111,255,0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
/* ── Header ── */
.app-header {
padding: 40px 0 28px;
text-align: center;
position: relative;
}
.app-header h1 {
font-family: 'Syne', sans-serif !important;
font-size: 2.4rem !important;
font-weight: 800 !important;
background: linear-gradient(135deg, #fff 0%, var(--accent) 50%, var(--accent2) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0 0 8px !important;
letter-spacing: -0.03em;
}
.app-header p {
color: var(--muted) !important;
font-size: 0.95rem !important;
margin: 0 !important;
}
/* ── Steps pill bar ── */
.steps-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.step-pill {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 100px;
padding: 7px 16px 7px 10px;
font-size: 0.82rem;
font-weight: 500;
color: var(--muted);
}
.step-pill .num {
width: 22px; height: 22px;
border-radius: 50%;
background: var(--accent);
color: #fff;
font-family: 'Syne', sans-serif;
font-weight: 700;
font-size: 0.75rem;
display: grid;
place-items: center;
}
.step-arrow { color: var(--border); font-size: 1rem; }
/* ── Panel cards ── */
.panel-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
}
/* ── Section labels ── */
.section-label {
font-family: 'Syne', sans-serif !important;
font-size: 0.7rem !important;
font-weight: 700 !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
color: var(--accent) !important;
margin-bottom: 10px !important;
display: flex;
align-items: center;
gap: 6px;
}
/* ── Inputs ── */
label > span {
font-family: 'DM Sans', sans-serif !important;
font-size: 0.78rem !important;
font-weight: 500 !important;
color: var(--muted) !important;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 5px !important;
}
textarea, input[type="text"], .gr-input, select {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-sm) !important;
color: var(--text) !important;
font-family: 'DM Sans', sans-serif !important;
font-size: 0.92rem !important;
transition: border-color 0.2s, box-shadow 0.2s;
}
textarea:focus, input[type="text"]:focus, .gr-input:focus {
border-color: var(--accent) !important;
box-shadow: 0 0 0 3px rgba(124,111,255,0.15) !important;
outline: none !important;
}
/* ── Dropdown ── */
.gr-dropdown, [data-testid="dropdown"] {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-sm) !important;
color: var(--text) !important;
}
/* ── Family radio tabs ── */
.family-tabs .gr-radio-group {
display: flex !important;
flex-wrap: wrap !important;
gap: 6px !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
}
.family-tabs .gr-radio-group label {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 100px !important;
padding: 6px 14px !important;
cursor: pointer !important;
font-size: 0.82rem !important;
font-weight: 500 !important;
color: var(--muted) !important;
transition: all 0.18s !important;
white-space: nowrap !important;
}
.family-tabs .gr-radio-group label:hover {
border-color: var(--accent) !important;
color: var(--text) !important;
}
.family-tabs .gr-radio-group label:has(input:checked) {
background: var(--accent) !important;
border-color: var(--accent) !important;
color: #fff !important;
}
/* ── LoRA info card ── */
.lora-info-box {
background: linear-gradient(135deg, rgba(124,111,255,0.08), rgba(255,111,216,0.05));
border: 1px solid rgba(124,111,255,0.25);
border-radius: var(--radius);
padding: 16px;
min-height: 80px;
}
.lora-info-box p, .lora-info-box h4 {
color: var(--text) !important;
}
/* ── Preview image ── */
.lora-preview img {
border-radius: var(--radius) !important;
object-fit: cover !important;
border: 1px solid var(--border) !important;
aspect-ratio: 1 / 1;
}
/* ── Generate button ── */
.generate-btn {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%) !important;
border: none !important;
border-radius: var(--radius) !important;
color: #fff !important;
font-family: 'Syne', sans-serif !important;
font-weight: 700 !important;
font-size: 1rem !important;
letter-spacing: 0.04em !important;
padding: 14px 28px !important;
cursor: pointer !important;
width: 100% !important;
height: 54px !important;
transition: opacity 0.2s, transform 0.15s, box-shadow 0.2s !important;
box-shadow: 0 4px 24px rgba(124,111,255,0.35) !important;
}
.generate-btn:hover {
opacity: 0.9 !important;
transform: translateY(-1px) !important;
box-shadow: 0 8px 32px rgba(124,111,255,0.5) !important;
}
.generate-btn:active {
transform: translateY(0) !important;
}
/* ── Result image ── */
.result-image img {
border-radius: var(--radius) !important;
border: 1px solid var(--border) !important;
box-shadow: var(--shadow) !important;
width: 100% !important;
}
/* ── Sliders ── */
.gr-slider input[type=range] {
accent-color: var(--accent) !important;
}
/* ── Accordion ── */
.gr-accordion {
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius) !important;
margin-top: 16px !important;
}
.gr-accordion-header {
font-family: 'Syne', sans-serif !important;
font-size: 0.85rem !important;
font-weight: 700 !important;
letter-spacing: 0.06em !important;
color: var(--muted) !important;
text-transform: uppercase;
padding: 14px 20px !important;
}
.gr-accordion-header:hover {
color: var(--text) !important;
}
/* ── Count badge ── */
.count-badge {
display: inline-block;
background: rgba(124,111,255,0.15);
color: var(--accent);
border-radius: 100px;
padding: 3px 10px;
font-size: 0.78rem;
font-weight: 600;
font-family: 'Syne', sans-serif;
}
.lora-gallery {
border-radius: var(--radius) !important;
overflow: hidden !important;
}
.lora-gallery .grid-wrap {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius) !important;
padding: 8px !important;
gap: 8px !important;
}
.lora-gallery .thumbnail-item {
border-radius: 10px !important;
overflow: hidden !important;
border: 2px solid transparent !important;
cursor: pointer !important;
transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s !important;
position: relative;
}
.lora-gallery .thumbnail-item:hover {
border-color: var(--accent) !important;
transform: scale(1.03) !important;
box-shadow: 0 4px 20px rgba(124,111,255,0.4) !important;
z-index: 2;
}
.lora-gallery .thumbnail-item.selected {
border-color: var(--accent2) !important;
box-shadow: 0 0 0 3px rgba(255,111,216,0.3) !important;
}
.lora-gallery .thumbnail-item img {
object-fit: cover !important;
width: 100% !important;
height: 100% !important;
display: block !important;
}
.lora-gallery .caption-label {
position: absolute !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
background: linear-gradient(transparent, rgba(0,0,0,0.85)) !important;
color: #fff !important;
font-size: 0.72rem !important;
font-family: 'DM Sans', sans-serif !important;
padding: 18px 8px 6px !important;
text-align: center !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--surface); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
/* ── Responsive ── */
@media (max-width: 768px) {
.app-header h1 { font-size: 1.7rem !important; }
.steps-bar { gap: 4px; }
.step-pill { font-size: 0.75rem; padding: 6px 12px 6px 8px; }
}
"""
INITIAL_FAMILY = "sdxl"
INITIAL_FILTERED = filter_loras(INITIAL_FAMILY, "")
INITIAL_SELECTED = INITIAL_FILTERED[0]["repo"] if INITIAL_FILTERED else None
with gr.Blocks() as app:
gr.HTML("""
<div class="app-header">
<h1>✦ LoRA Playground</h1>
<p>by artificialguybr · Generate images with custom LoRA adapters</p>
</div>
<div class="steps-bar">
<span class="step-pill"><span class="num">1</span>Pick model family</span>
<span class="step-arrow">›</span>
<span class="step-pill"><span class="num">2</span>Choose a LoRA</span>
<span class="step-arrow">›</span>
<span class="step-pill"><span class="num">3</span>Write your prompt</span>
<span class="step-arrow">›</span>
<span class="step-pill"><span class="num">4</span>Generate</span>
</div>
""")
selected_repo = gr.State(INITIAL_SELECTED)
with gr.Row(equal_height=False):
with gr.Column(scale=1, min_width=320):
gr.HTML('<p class="section-label">⬡ Step 1 · Model Family</p>')
family_filter = gr.Dropdown(
label="",
choices=[(label, key) for key, label in FAMILY_LABELS.items() if key != "all"],
value=INITIAL_FAMILY,
container=False,
)
gr.HTML('<div style="height:16px"></div>')
gr.HTML('<p class="section-label">◈ Step 2 · Choose LoRA</p>')
search_box = gr.Textbox(
label="",
placeholder="🔍 Search by name, style, keyword…",
container=False,
)
gr.HTML('<div style="height:8px"></div>')
catalog_stats = gr.Markdown(
f"**{len(INITIAL_FILTERED)}** LoRAs · SDXL",
elem_classes=["count-badge"],
)
gr.HTML('<div style="height:6px"></div>')
lora_gallery = gr.Gallery(
label="",
value=build_gallery_items(INITIAL_FILTERED),
columns=2,
rows=4,
height=420,
object_fit="cover",
allow_preview=False,
container=False,
elem_classes=["lora-gallery"],
)
gr.HTML('<div style="height:20px"></div>')
gr.HTML('<p class="section-label">◎ LoRA Details</p>')
lora_preview = gr.Image(
label="",
height=240,
container=False,
elem_classes=["lora-preview"],
)
gr.HTML('<div style="height:10px"></div>')
selected_info = gr.Markdown("", elem_classes=["lora-info-box"])
with gr.Column(scale=2):
gr.HTML('<p class="section-label">✏ Step 3 · Your Prompt</p>')
prompt = gr.Textbox(
label="",
lines=3,
placeholder="Select a LoRA to begin…",
container=False,
)
gr.HTML('<div style="height:8px"></div>')
negative_prompt = gr.Textbox(
label="Negative prompt",
lines=2,
value=DEFAULT_NEGATIVE,
)
gr.HTML('<div style="height:14px"></div>')
generate_button = gr.Button(
"✦ Generate Image",
variant="primary",
elem_classes=["generate-btn"],
)
gr.HTML('<div style="height:16px"></div>')
result = gr.Image(
label="",
height=640,
container=False,
elem_classes=["result-image"],
)
with gr.Accordion("⚙ Advanced Settings", open=False):
with gr.Row():
cfg_scale = gr.Slider(label="CFG Scale", minimum=1, maximum=20, step=0.5, value=7.0)
steps = gr.Slider(label="Steps", minimum=1, maximum=100, step=1, value=30)
with gr.Row():
width = gr.Slider(label="Width", minimum=256, maximum=1536, step=8, value=1024)
height = gr.Slider(label="Height", minimum=256, maximum=1536, step=8, value=1024)
with gr.Row():
seed = gr.Slider(
label="Seed",
minimum=0,
maximum=2**32 - 1,
step=1,
value=0,
randomize=True,
)
lora_scale = gr.Slider(label="LoRA Scale", minimum=0, maximum=1.5, step=0.01, value=1.0)
scheduler = gr.Dropdown(
label="Scheduler (SD families only)",
choices=SCHEDULER_CHOICES,
value="Auto",
interactive=False,
)
_selector_outputs = [
lora_gallery,
catalog_stats,
lora_preview,
selected_info,
prompt,
selected_repo,
steps,
cfg_scale,
width,
height,
scheduler,
lora_scale,
]
_payload_outputs = [
lora_preview,
selected_info,
prompt,
selected_repo,
steps,
cfg_scale,
width,
height,
scheduler,
lora_scale,
]
_initial_payload = selected_payload(INITIAL_SELECTED)
family_filter.change(fn=refresh_lora_selector, inputs=[family_filter, search_box], outputs=_selector_outputs)
search_box.change(fn=refresh_lora_selector, inputs=[family_filter, search_box], outputs=_selector_outputs)
lora_gallery.select(fn=on_gallery_select, inputs=[family_filter, search_box], outputs=_payload_outputs)
app.load(fn=lambda: _initial_payload, outputs=_payload_outputs)
generate_button.click(
fn=run_lora,
inputs=[prompt, negative_prompt, cfg_scale, steps, selected_repo, scheduler, seed, width, height, lora_scale],
outputs=[result],
)
app.queue(max_size=20)
app.launch(theme=gr.themes.Base(), css=CSS)