Spaces:
Running on Zero
Running on Zero
Upload 2 files
Browse files- app.py +53 -375
- lora_registry.py +10 -13
app.py
CHANGED
|
@@ -6,19 +6,33 @@ import random
|
|
| 6 |
import uuid
|
| 7 |
import zipfile
|
| 8 |
import threading
|
| 9 |
-
from typing import Iterable
|
| 10 |
|
| 11 |
import gradio as gr
|
| 12 |
-
import numpy as np
|
| 13 |
import spaces
|
| 14 |
import torch
|
| 15 |
from PIL import Image
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from logging_utils import log_inference
|
| 18 |
-
|
| 19 |
from image_utils import (
|
| 20 |
fix_orientation,
|
| 21 |
-
compute_base_dimensions,
|
| 22 |
compute_canvas_dimensions,
|
| 23 |
fit_to_canvas,
|
| 24 |
on_base_image_change,
|
|
@@ -35,7 +49,6 @@ from image_utils import (
|
|
| 35 |
push_pil_to_base,
|
| 36 |
push_pil_to_reference,
|
| 37 |
)
|
| 38 |
-
|
| 39 |
from control_tools import (
|
| 40 |
generate_depthmap,
|
| 41 |
detect_pose,
|
|
@@ -51,244 +64,23 @@ from control_tools import (
|
|
| 51 |
OPENPOSE_KEYPOINT_NAMES,
|
| 52 |
)
|
| 53 |
|
| 54 |
-
#
|
| 55 |
-
|
| 56 |
-
|
| 57 |
if MODEL_VARIANT == "9B-KV":
|
| 58 |
from diffusers import Flux2KleinKVPipeline as _PipeClass
|
| 59 |
-
_MODEL_REPO = "black-forest-labs/FLUX.2-klein-9b-kv"
|
| 60 |
else:
|
| 61 |
from diffusers import Flux2KleinPipeline as _PipeClass
|
| 62 |
-
_MODEL_REPO = "black-forest-labs/FLUX.2-klein-9B"
|
| 63 |
-
MODEL_VARIANT = "9B"
|
| 64 |
|
| 65 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
from gradio.themes.utils import colors, fonts, sizes
|
| 70 |
-
|
| 71 |
-
colors.orange_red = colors.Color(
|
| 72 |
-
name="orange_red", c50="#FFF0E5", c100="#FFE0CC", c200="#FFC299", c300="#FFA366",
|
| 73 |
-
c400="#FF8533", c500="#FF4500", c600="#E63E00", c700="#CC3700", c800="#B33000",
|
| 74 |
-
c900="#992900", c950="#802200",
|
| 75 |
-
)
|
| 76 |
-
|
| 77 |
-
class OrangeRedTheme(Soft):
|
| 78 |
-
def __init__(self, *, primary_hue=colors.gray, secondary_hue=colors.orange_red,
|
| 79 |
-
neutral_hue=colors.slate, text_size=sizes.text_lg,
|
| 80 |
-
font=(fonts.GoogleFont("Outfit"), "Arial", "sans-serif"),
|
| 81 |
-
font_mono=(fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace")):
|
| 82 |
-
super().__init__(primary_hue=primary_hue, secondary_hue=secondary_hue,
|
| 83 |
-
neutral_hue=neutral_hue, text_size=text_size,
|
| 84 |
-
font=font, font_mono=font_mono)
|
| 85 |
-
super().set(
|
| 86 |
-
background_fill_primary="*primary_50",
|
| 87 |
-
background_fill_primary_dark="*primary_900",
|
| 88 |
-
body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
|
| 89 |
-
body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
|
| 90 |
-
button_primary_text_color="white",
|
| 91 |
-
button_primary_text_color_hover="white",
|
| 92 |
-
button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
|
| 93 |
-
button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
|
| 94 |
-
button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
|
| 95 |
-
button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
|
| 96 |
-
slider_color="*secondary_500", slider_color_dark="*secondary_600",
|
| 97 |
-
block_title_text_weight="600", block_border_width="3px",
|
| 98 |
-
block_shadow="*shadow_drop_lg", button_primary_shadow="*shadow_drop_lg",
|
| 99 |
-
button_large_padding="11px", color_accent_soft="*primary_100",
|
| 100 |
-
block_label_background_fill="*primary_200",
|
| 101 |
-
)
|
| 102 |
-
|
| 103 |
-
orange_red_theme = OrangeRedTheme()
|
| 104 |
-
MAX_SEED = np.iinfo(np.int32).max
|
| 105 |
-
|
| 106 |
-
# ── Upscaler models, FACE_SWAP_PROMPT, LORA_STYLES — UNCHANGED from previous step
|
| 107 |
-
UPSCALE_MODELS = {
|
| 108 |
-
"None": {"scale": None, "file": None, "url": None},
|
| 109 |
-
"2× — RealESRGAN (balanced)": {"scale": 2, "file": "RealESRGAN_x2plus.pth",
|
| 110 |
-
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth"},
|
| 111 |
-
"4× — RealESRGAN (balanced)": {"scale": 4, "file": "RealESRGAN_x4plus.pth",
|
| 112 |
-
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth"},
|
| 113 |
-
"4× — UltraSharp (crisp)": {"scale": 4, "file": "4x-UltraSharpV2.pth",
|
| 114 |
-
"url": "https://huggingface.co/Kim2091/UltraSharpV2/resolve/main/4x-UltraSharpV2.pth"},
|
| 115 |
-
"4× — Remacri (natural)": {"scale": 4, "file": "4x_foolhardy_Remacri.pth",
|
| 116 |
-
"url": "https://huggingface.co/FacehugmanIII/4x_foolhardy_Remacri/resolve/main/4x_foolhardy_Remacri.pth"},
|
| 117 |
-
"4× — Nomos2 HQ DAT2 (Photography)": {"scale": 4, "file": "4xNomos2_hq_dat2.pth",
|
| 118 |
-
"url": "https://github.com/Phhofm/models/releases/download/4xNomos2_hq_dat2/4xNomos2_hq_dat2.pth"},
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
FACE_SWAP_PROMPT = """head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. Remove the head from Picture 1 completely and replace it with the head from Picture 2.
|
| 122 |
-
FROM PICTURE 1 (strictly preserve):
|
| 123 |
-
- Scene: lighting conditions, shadows, highlights, color temperature, environment, background
|
| 124 |
-
- Head positioning: exact rotation angle, tilt, direction the head is facing
|
| 125 |
-
- Expression: facial expression, micro-expressions, eye gaze direction, mouth position, emotion
|
| 126 |
-
FROM PICTURE 2 (strictly preserve identity):
|
| 127 |
-
- Facial structure: face shape, bone structure, jawline, chin
|
| 128 |
-
- All facial features: eye color, eye shape, nose structure, lip shape and fullness, eyebrows
|
| 129 |
-
- Hair: color, style, texture, hairline
|
| 130 |
-
- Skin: texture, tone, complexion
|
| 131 |
-
The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k."""
|
| 132 |
-
|
| 133 |
-
MAX_LORA_SLOTS = 6
|
| 134 |
-
|
| 135 |
-
LORA_STYLES = [
|
| 136 |
-
{
|
| 137 |
-
"image": "https://huggingface.co/spaces/prithivMLmods/FLUX.2-Klein-LoRA-Studio/resolve/main/examples/image.webp",
|
| 138 |
-
"title": "None",
|
| 139 |
-
"adapter_name": None,
|
| 140 |
-
"repo": None,
|
| 141 |
-
"weights": None,
|
| 142 |
-
"default_prompt": None,
|
| 143 |
-
"default_weight": 1.0,
|
| 144 |
-
},
|
| 145 |
-
{
|
| 146 |
-
"title": "Klein-Delight-Style",
|
| 147 |
-
"adapter_name": "klein-delight",
|
| 148 |
-
"repo": "linoyts/Flux2-Klein-Delight-LoRA",
|
| 149 |
-
"weights": "pytorch_lora_weights.safetensors",
|
| 150 |
-
"default_prompt": "Relight the image to remove all existing lighting conditions and replace them with neutral, uniform illumination. Apply soft, evenly distributed lighting with no directional shadows, no harsh highlights, and no dramatic contrast. Maintain the original identity of all subjects exactly—preserve facial structure, skin tone, proportions, expressions, hair, clothing, and textures. Do not alter pose, camera angle, background geometry, or image composition. Lighting should appear balanced, and studio-neutral, similar to diffuse overcast or a soft lightbox setup. Ensure consistent exposure across the entire image with realistic depth and subtle shading only where necessary for form.",
|
| 151 |
-
"default_weight": 1.0,
|
| 152 |
-
},
|
| 153 |
-
{
|
| 154 |
-
"title": "Klein-Consistency",
|
| 155 |
-
"adapter_name": "klein-consistency",
|
| 156 |
-
"repo": "dx8152/Flux2-Klein-9B-Consistency",
|
| 157 |
-
"weights": "Klein-consistency.safetensors",
|
| 158 |
-
"default_prompt": None,
|
| 159 |
-
"default_weight": 0.3,
|
| 160 |
-
},
|
| 161 |
-
{
|
| 162 |
-
"title": "Best-Face-Swap",
|
| 163 |
-
"adapter_name": "face-swap",
|
| 164 |
-
"repo": "Alissonerdx/BFS-Best-Face-Swap",
|
| 165 |
-
"weights": "bfs_head_v1_flux-klein_9b_step3750_rank64.safetensors",
|
| 166 |
-
"default_prompt": FACE_SWAP_PROMPT,
|
| 167 |
-
"default_weight": 1.0,
|
| 168 |
-
},
|
| 169 |
-
{
|
| 170 |
-
"title": "NSFW v2",
|
| 171 |
-
"adapter_name": "nsfw-v2",
|
| 172 |
-
"repo": "diroverflo/FLux_Klein_9B_NSFW",
|
| 173 |
-
"weights": "Flux Klein - NSFW v2.safetensors",
|
| 174 |
-
"default_prompt": None,
|
| 175 |
-
"default_weight": 1.0,
|
| 176 |
-
},
|
| 177 |
-
{
|
| 178 |
-
"title": "Ultimate Upscaler Klein-9b",
|
| 179 |
-
"adapter_name": "Ultimate Upscaler",
|
| 180 |
-
"repo": "loras",
|
| 181 |
-
"weights": "Flux2-Klein-Image-RestoreV1.safetensors",
|
| 182 |
-
"default_prompt": "restore the image quality, remove any compression artefacts, remove any haze and soft edges, enrich the original with new intricate detail in all textures and surfaces creating a professional photorealistic photograph with natural lighting and skin texture.",
|
| 183 |
-
"default_weight": 1.0,
|
| 184 |
-
},
|
| 185 |
-
{
|
| 186 |
-
"title": "High Resolution",
|
| 187 |
-
"adapter_name": "High Resolution",
|
| 188 |
-
"repo": "loras",
|
| 189 |
-
"weights": "HighResolution9B.safetensors",
|
| 190 |
-
"default_prompt": "High Resolution",
|
| 191 |
-
"default_weight": 1.0,
|
| 192 |
-
},
|
| 193 |
-
{
|
| 194 |
-
"title": "InstaPic",
|
| 195 |
-
"adapter_name": "InstaPic V3",
|
| 196 |
-
"repo": "loras",
|
| 197 |
-
"weights": "InstaPic V3.safetensors",
|
| 198 |
-
"default_prompt": "instapic",
|
| 199 |
-
"default_weight": 1.0,
|
| 200 |
-
},
|
| 201 |
-
{
|
| 202 |
-
"title": "Realistic Nudes",
|
| 203 |
-
"adapter_name": "Realistic Nudes",
|
| 204 |
-
"repo": "loras",
|
| 205 |
-
"weights": "realistic_nudes_klein_v3.safetensors",
|
| 206 |
-
"default_prompt": None,
|
| 207 |
-
"default_weight": 1.0,
|
| 208 |
-
},
|
| 209 |
-
{
|
| 210 |
-
"title": "Perky Pointy Puffy Breasts",
|
| 211 |
-
"adapter_name": "Perky Pointy Puffy Breasts",
|
| 212 |
-
"repo": "loras",
|
| 213 |
-
"weights": "PerkyPointyPuffy_v1.1_small_pointy_breasts_large_puffy_nipples.safetensors",
|
| 214 |
-
"default_prompt": "Small pointy breasts with large puffy nipples",
|
| 215 |
-
"default_weight": 1.0,
|
| 216 |
-
},
|
| 217 |
-
{
|
| 218 |
-
"title": "Flat Chested",
|
| 219 |
-
"adapter_name": "Flat Chested",
|
| 220 |
-
"repo": "loras",
|
| 221 |
-
"weights": "Flux2-Klein-9b-FlatChested-v1.safetensors",
|
| 222 |
-
"default_prompt": "flat chested",
|
| 223 |
-
"default_weight": 1.5,
|
| 224 |
-
},
|
| 225 |
-
{
|
| 226 |
-
"title": "Controllight",
|
| 227 |
-
"adapter_name": "Controllight",
|
| 228 |
-
"repo": "ControlLight/ControlLight",
|
| 229 |
-
"weights": "controllight.safetensors",
|
| 230 |
-
"default_prompt": None,
|
| 231 |
-
"default_weight": 1.0,
|
| 232 |
-
},
|
| 233 |
-
{
|
| 234 |
-
"title": "RefControl - Depth",
|
| 235 |
-
"adapter_name": "RefConDep",
|
| 236 |
-
"repo": "thedeoxen/refcontrol-FLUX.2-klein-9B-reference-depth-lora",
|
| 237 |
-
"weights": "flux2_klein_9b_refcontrol_depth.safetensors",
|
| 238 |
-
"default_prompt": "refcontrol",
|
| 239 |
-
"default_weight": 1.0,
|
| 240 |
-
},
|
| 241 |
-
{
|
| 242 |
-
"title": "RefControl - Pose",
|
| 243 |
-
"adapter_name": "RefConPos",
|
| 244 |
-
"repo": "thedeoxen/refcontrol-FLUX.2-klein-9B-reference-pose-lora",
|
| 245 |
-
"weights": "refcontrol_v2_poses.safetensors",
|
| 246 |
-
"default_prompt": "apply pose from image 1 with reference from image 2",
|
| 247 |
-
"default_weight": 1.0,
|
| 248 |
-
},
|
| 249 |
-
]
|
| 250 |
-
|
| 251 |
-
LOADED_ADAPTERS = set()
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
def get_all_styles(dynamic_loras):
|
| 255 |
-
return list(LORA_STYLES) + list((dynamic_loras or {}).values())
|
| 256 |
-
|
| 257 |
-
def get_selectable_styles(dynamic_loras):
|
| 258 |
-
return [s for s in get_all_styles(dynamic_loras) if s["adapter_name"] is not None]
|
| 259 |
-
|
| 260 |
-
def get_style_by_title(title, dynamic_loras):
|
| 261 |
-
for s in get_all_styles(dynamic_loras):
|
| 262 |
-
if s["title"] == title:
|
| 263 |
-
return s
|
| 264 |
-
return None
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
print(f"Loading FLUX.2 Klein {MODEL_VARIANT} from {_MODEL_REPO}...")
|
| 268 |
-
pipe = _PipeClass.from_pretrained(_MODEL_REPO, torch_dtype=torch.bfloat16).to(device)
|
| 269 |
print(f"Model loaded successfully: FLUX.2 Klein {MODEL_VARIANT}")
|
| 270 |
|
| 271 |
|
| 272 |
# ── UI helper callbacks ──────────────────────────────────────────────────────
|
| 273 |
|
| 274 |
-
def update_weight_sliders(selected_titles, dynamic_loras):
|
| 275 |
-
selected = [get_style_by_title(t, dynamic_loras) for t in (selected_titles or [])
|
| 276 |
-
if get_style_by_title(t, dynamic_loras)]
|
| 277 |
-
updates = []
|
| 278 |
-
for i in range(MAX_LORA_SLOTS):
|
| 279 |
-
if i < len(selected):
|
| 280 |
-
s = selected[i]
|
| 281 |
-
updates.append(gr.update(visible=True,
|
| 282 |
-
label=f"{s['title']} — weight",
|
| 283 |
-
value=s.get("default_weight", 1.0)))
|
| 284 |
-
else:
|
| 285 |
-
updates.append(gr.update(visible=False, value=1.0))
|
| 286 |
-
prompts = [s.get("default_prompt") for s in selected if s.get("default_prompt")]
|
| 287 |
-
if prompts:
|
| 288 |
-
return updates + [gr.update(value="\n\n".join(prompts), visible=True)]
|
| 289 |
-
return updates + [gr.update(value="", visible=False)]
|
| 290 |
-
|
| 291 |
-
|
| 292 |
def on_canvas_mode_change(mode):
|
| 293 |
"""Custom W/H sliders only relevant when mode == Custom."""
|
| 294 |
is_custom = (mode == "Custom")
|
|
@@ -317,65 +109,16 @@ def on_gallery_select(evt: gr.SelectData, gallery_value):
|
|
| 317 |
return item[0] if isinstance(item, (list, tuple)) else item
|
| 318 |
|
| 319 |
|
| 320 |
-
# ── Upscaler (tiled) — unchanged ─────────────────────────────────────────────
|
| 321 |
-
|
| 322 |
-
def _upscale_tiled(model_fn, img_t, tile=512, overlap=32):
|
| 323 |
-
_, c, h, w = img_t.shape
|
| 324 |
-
with torch.no_grad():
|
| 325 |
-
probe = model_fn(img_t[:, :, :min(4, h), :min(4, w)])
|
| 326 |
-
scale = probe.shape[-1] // min(4, w)
|
| 327 |
-
del probe; torch.cuda.empty_cache()
|
| 328 |
-
out_h, out_w = h * scale, w * scale
|
| 329 |
-
canvas = torch.zeros(1, c, out_h, out_w, dtype=torch.float32)
|
| 330 |
-
weights = torch.zeros(1, 1, out_h, out_w, dtype=torch.float32)
|
| 331 |
-
step = max(tile - overlap, 1)
|
| 332 |
-
ys = sorted(set(list(range(0, max(h - tile, 0), step)) + [max(h - tile, 0)]))
|
| 333 |
-
xs = sorted(set(list(range(0, max(w - tile, 0), step)) + [max(w - tile, 0)]))
|
| 334 |
-
for y0 in ys:
|
| 335 |
-
for x0 in xs:
|
| 336 |
-
y1 = min(y0 + tile, h); x1 = min(x0 + tile, w)
|
| 337 |
-
with torch.no_grad():
|
| 338 |
-
out = model_fn(img_t[:, :, y0:y1, x0:x1]).cpu().float()
|
| 339 |
-
oy0, ox0 = y0 * scale, x0 * scale
|
| 340 |
-
oy1, ox1 = y1 * scale, x1 * scale
|
| 341 |
-
canvas[:, :, oy0:oy1, ox0:ox1] += out
|
| 342 |
-
weights[:, :, oy0:oy1, ox0:ox1] += 1.0
|
| 343 |
-
return (canvas / weights.clamp(min=1)).clamp(0, 1)
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
def apply_realesrgan(image, model_key):
|
| 347 |
-
cfg = UPSCALE_MODELS[model_key]
|
| 348 |
-
try:
|
| 349 |
-
from spandrel import ImageModelDescriptor, ModelLoader
|
| 350 |
-
except ImportError:
|
| 351 |
-
raise gr.Error("spandrel is not installed. Add 'spandrel' to requirements.txt.")
|
| 352 |
-
import urllib.request
|
| 353 |
-
cache_dir = "/tmp/realesrgan_weights"
|
| 354 |
-
os.makedirs(cache_dir, exist_ok=True)
|
| 355 |
-
cache_path = os.path.join(cache_dir, cfg["file"])
|
| 356 |
-
if not os.path.exists(cache_path):
|
| 357 |
-
print(f"Downloading {cfg['file']}…")
|
| 358 |
-
urllib.request.urlretrieve(cfg["url"], cache_path)
|
| 359 |
-
model = ModelLoader().load_from_file(cache_path)
|
| 360 |
-
if not isinstance(model, ImageModelDescriptor):
|
| 361 |
-
raise gr.Error(f"Loaded model is not a single-image descriptor: {type(model)}")
|
| 362 |
-
sr_model = model.model.to(device).eval()
|
| 363 |
-
img_np = np.array(image).astype(np.float32) / 255.0
|
| 364 |
-
img_t = torch.from_numpy(img_np).permute(2, 0, 1).unsqueeze(0).to(device)
|
| 365 |
-
out_t = _upscale_tiled(sr_model, img_t, tile=512, overlap=32)
|
| 366 |
-
del sr_model, img_t
|
| 367 |
-
gc.collect(); torch.cuda.empty_cache()
|
| 368 |
-
out_np = out_t.squeeze(0).permute(1, 2, 0).numpy()
|
| 369 |
-
return Image.fromarray((out_np * 255).astype(np.uint8))
|
| 370 |
-
|
| 371 |
-
|
| 372 |
# ── Logging ──────────────────────────────────────────────────────────────────
|
| 373 |
|
| 374 |
def _spawn_log(pil_images, result_image, prompt, seed, steps, guidance_scale,
|
| 375 |
width, height, duration, success, error="",
|
| 376 |
lora_titles=None, lora_weights=None, upscale_factor="None",
|
| 377 |
lora_prompt_text=""):
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
| 379 |
return
|
| 380 |
threading.Thread(
|
| 381 |
target=log_inference,
|
|
@@ -449,7 +192,7 @@ def _infer_gpu(
|
|
| 449 |
if upscale_factor and upscale_factor != "None":
|
| 450 |
gc.collect(); torch.cuda.synchronize(); torch.cuda.empty_cache()
|
| 451 |
try:
|
| 452 |
-
image = apply_realesrgan(image, upscale_factor)
|
| 453 |
except Exception as e:
|
| 454 |
gr.Warning(f"Upscaling failed, returning {width}×{height} result: {e}")
|
| 455 |
|
|
@@ -502,8 +245,6 @@ def infer(
|
|
| 502 |
selected_titles = selected_titles or []
|
| 503 |
batch_count = max(1, int(batch_count))
|
| 504 |
|
| 505 |
-
# Pre-plan per-iteration seed and weight overrides — keeps the loop simple
|
| 506 |
-
# and lets us put the LoRA-sweep values into PNG metadata cleanly.
|
| 507 |
base_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
|
| 508 |
seeds, weight_overrides = [], []
|
| 509 |
for i in range(batch_count):
|
|
@@ -582,7 +323,12 @@ def bulk_infer(
|
|
| 582 |
into the bulk output gallery as soon as they complete. Outputs and a CSV
|
| 583 |
manifest are written to /tmp/bulk_<sid>/ with stable filenames, so a
|
| 584 |
ZeroGPU quota wall mid-run still leaves earlier outputs grabbable from
|
| 585 |
-
the gallery and from disk.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
if not input_files:
|
| 587 |
raise gr.Error("Upload at least one image first.")
|
| 588 |
|
|
@@ -619,8 +365,6 @@ def bulk_infer(
|
|
| 619 |
*slider_values, progress=progress,
|
| 620 |
)
|
| 621 |
|
| 622 |
-
# Stable, predictable filename inside the session work_dir so the
|
| 623 |
-
# user can also find outputs on disk if the UI drops.
|
| 624 |
stem = os.path.splitext(fname)[0]
|
| 625 |
out_path = os.path.join(work_dir, f"{i:03d}_{stem}.png")
|
| 626 |
meta = _meta_for(prompt, used_seed, steps, guidance_scale, w, h, upscale_factor,
|
|
@@ -642,16 +386,19 @@ def bulk_infer(
|
|
| 642 |
upscale_factor=upscale_factor, lora_prompt_text=lora_prompt_text or "")
|
| 643 |
|
| 644 |
status = f"✅ {succeeded}/{total} done ({failed} failed)"
|
| 645 |
-
yield results, status, gr.update()
|
| 646 |
except Exception as e:
|
| 647 |
failed += 1
|
| 648 |
duration = time.perf_counter() - t0
|
| 649 |
with open(manifest_path, "a", newline="") as f:
|
| 650 |
csv.writer(f).writerow([i, fname, "", "", "", "", False, str(e)[:300], f"{duration:.2f}"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
yield results, f"⚠️ Image {i+1} failed: {e} | {succeeded} ok, {failed} failed", gr.update()
|
| 652 |
continue
|
| 653 |
|
| 654 |
-
# Final pass: bundle a zip. ZIP_STORED because PNGs are already compressed.
|
| 655 |
zip_path = os.path.join(work_dir, "outputs.zip")
|
| 656 |
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_STORED) as zf:
|
| 657 |
for p in results:
|
|
@@ -661,44 +408,7 @@ def bulk_infer(
|
|
| 661 |
yield results, f"🎉 Done — {succeeded}/{total} succeeded, {failed} failed", zip_path
|
| 662 |
|
| 663 |
|
| 664 |
-
# ──
|
| 665 |
-
|
| 666 |
-
def add_custom_lora(repo_id, weight_name, adapter_name, dynamic_loras_state):
|
| 667 |
-
dynamic_loras = dict(dynamic_loras_state or {})
|
| 668 |
-
if not repo_id or not repo_id.strip():
|
| 669 |
-
return "Please enter a valid HuggingFace repo ID.", gr.update(), dynamic_loras
|
| 670 |
-
repo_id = repo_id.strip()
|
| 671 |
-
requested_name = adapter_name.strip() if adapter_name and adapter_name.strip() else None
|
| 672 |
-
try:
|
| 673 |
-
from huggingface_hub import model_info
|
| 674 |
-
info = model_info(repo_id)
|
| 675 |
-
actual_weight = weight_name.strip() if weight_name and weight_name.strip() else None
|
| 676 |
-
if not actual_weight:
|
| 677 |
-
for name in ["pytorch_lora_weights.safetensors", "lora.safetensors", "adapter_model.safetensors"]:
|
| 678 |
-
if any(f.filename == name for f in info.siblings):
|
| 679 |
-
actual_weight = name
|
| 680 |
-
break
|
| 681 |
-
if not actual_weight:
|
| 682 |
-
available = [f.filename for f in info.siblings if f.filename.endswith(('.safetensors', '.bin'))]
|
| 683 |
-
return f"No weight found. Available: {', '.join(available) or 'None'}", gr.update(), dynamic_loras
|
| 684 |
-
base_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in requested_name) if requested_name else "custom"
|
| 685 |
-
static_names = {s["adapter_name"] for s in LORA_STYLES if s["adapter_name"]}
|
| 686 |
-
final = f"{base_name}_{uuid.uuid4().hex[:6]}"
|
| 687 |
-
while final in static_names or final in LOADED_ADAPTERS:
|
| 688 |
-
final = f"{base_name}_{uuid.uuid4().hex[:6]}"
|
| 689 |
-
dynamic_loras[final] = {
|
| 690 |
-
"image": "https://huggingface.co/spaces/prithivMLmods/FLUX.2-Klein-LoRA-Studio/resolve/main/examples/image.webp",
|
| 691 |
-
"title": f"Custom: {base_name}", "adapter_name": final,
|
| 692 |
-
"repo": repo_id, "weights": actual_weight,
|
| 693 |
-
"default_prompt": None, "default_weight": 1.0,
|
| 694 |
-
}
|
| 695 |
-
new_choices = [s["title"] for s in get_selectable_styles(dynamic_loras)]
|
| 696 |
-
return f"✅ Added: {base_name} from {repo_id}", gr.update(choices=new_choices), dynamic_loras
|
| 697 |
-
except Exception as e:
|
| 698 |
-
return f"❌ Failed: {str(e)}", gr.update(), dynamic_loras
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
# ── Custom prompt manager (unchanged) ────────────────────────────────────────
|
| 702 |
|
| 703 |
def add_custom_prompt(name, text, prompts_state, counter_state):
|
| 704 |
prompts = dict(prompts_state); counter = int(counter_state)
|
|
@@ -716,6 +426,7 @@ def add_custom_prompt(name, text, prompts_state, counter_state):
|
|
| 716 |
gr.update(choices=choices), gr.update(choices=choices),
|
| 717 |
gr.update(value="", interactive=True))
|
| 718 |
|
|
|
|
| 719 |
def delete_custom_prompt(name, currently_selected, prompts_state):
|
| 720 |
prompts = dict(prompts_state)
|
| 721 |
msg = f"🗑️ Deleted: '{name}'" if name and name in prompts else "Nothing to delete."
|
|
@@ -727,6 +438,7 @@ def delete_custom_prompt(name, currently_selected, prompts_state):
|
|
| 727 |
gr.update(choices=choices, value=new_sel),
|
| 728 |
gr.update(choices=choices, value=None))
|
| 729 |
|
|
|
|
| 730 |
def update_custom_prompt_display(selected_names, prompts_state):
|
| 731 |
if not selected_names:
|
| 732 |
return gr.update(value="", visible=False)
|
|
@@ -750,16 +462,15 @@ with gr.Blocks() as demo:
|
|
| 750 |
custom_prompts_state = gr.State({})
|
| 751 |
custom_prompt_counter_state = gr.State(0)
|
| 752 |
dynamic_loras_state = gr.State({})
|
| 753 |
-
selected_output_state = gr.State(None)
|
| 754 |
|
| 755 |
with gr.Column(elem_id="col-container"):
|
| 756 |
-
|
| 757 |
-
_logging_badge = "🟢 On" if _logging_on else "🔴 Off"
|
| 758 |
|
| 759 |
gr.Markdown("# **FLUX.2-Klein-LoRA-Studio**", elem_id="main-title")
|
| 760 |
gr.Markdown(
|
| 761 |
f"Apply one or more [LoRA](https://huggingface.co/models?other=base_model:adapter:black-forest-labs/FLUX.2-klein-9B) "
|
| 762 |
-
f"adapters using [FLUX.2-Klein-{MODEL_VARIANT}]({
|
| 763 |
f"**Model:** `{MODEL_VARIANT}` · **Logging:** {_logging_badge}"
|
| 764 |
)
|
| 765 |
|
|
@@ -835,11 +546,7 @@ with gr.Blocks() as demo:
|
|
| 835 |
sweep_max = gr.Slider(label="Sweep max weight", minimum=0.0, maximum=2.0,
|
| 836 |
step=0.05, value=1.4, visible=False)
|
| 837 |
|
| 838 |
-
# ── Right column ─────────────────────────────────────────
|
| 839 |
with gr.Column(scale=1):
|
| 840 |
-
# Gallery so batch runs stream in as they finish; type=
|
| 841 |
-
# filepath so PNG metadata round-trips to Send→* buttons
|
| 842 |
-
# and to the user's downloads.
|
| 843 |
output_gallery = gr.Gallery(
|
| 844 |
label="Output", type="filepath", columns=2, rows=2,
|
| 845 |
height=420, allow_preview=True, preview=True,
|
|
@@ -857,7 +564,6 @@ with gr.Blocks() as demo:
|
|
| 857 |
"Civitai / ComfyUI readable).*"
|
| 858 |
)
|
| 859 |
|
| 860 |
-
# LoRA selector + weight sliders
|
| 861 |
gr.Markdown("### 🎨 Select LoRA(s)")
|
| 862 |
lora_selector = gr.CheckboxGroup(
|
| 863 |
choices=[s["title"] for s in get_selectable_styles({})],
|
|
@@ -967,10 +673,8 @@ with gr.Blocks() as demo:
|
|
| 967 |
"Reference image."
|
| 968 |
)
|
| 969 |
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
pose_source_state = gr.State(None) # PIL of last source image
|
| 973 |
-
pose_keypoints_state = gr.State([]) # list[list[dict]]
|
| 974 |
|
| 975 |
with gr.Row():
|
| 976 |
with gr.Column(scale=1):
|
|
@@ -1000,8 +704,6 @@ with gr.Blocks() as demo:
|
|
| 1000 |
|
| 1001 |
with gr.Row():
|
| 1002 |
with gr.Column(scale=1):
|
| 1003 |
-
# interactive=False so users can't accidentally upload a new image
|
| 1004 |
-
# into this slot. .select still fires for click coordinates.
|
| 1005 |
pose_overlay = gr.Image(
|
| 1006 |
label="Editor — click to place active joint",
|
| 1007 |
type="pil", interactive=False, height=420, format="png",
|
|
@@ -1031,21 +733,21 @@ with gr.Blocks() as demo:
|
|
| 1031 |
send_pose_ref_btn = gr.Button("→ Send pose to Reference",
|
| 1032 |
variant="primary")
|
| 1033 |
send_pose_base_btn = gr.Button("→ Send pose to Base")
|
| 1034 |
-
|
| 1035 |
# ── Event wiring ─────────────────────────────────────────────────────────
|
| 1036 |
|
| 1037 |
-
# HEIC preview fix on the main tab
|
| 1038 |
base_image.upload(fn=reencode_upload, inputs=[base_image], outputs=[base_image])
|
| 1039 |
-
|
| 1040 |
base_image.change(fn=on_base_image_change, inputs=[base_image], outputs=[size_info])
|
| 1041 |
reference_images.change(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
|
| 1042 |
|
|
|
|
| 1043 |
lora_selector.change(
|
| 1044 |
fn=update_weight_sliders,
|
| 1045 |
inputs=[lora_selector, dynamic_loras_state],
|
| 1046 |
outputs=weight_sliders + [lora_prompt_display],
|
| 1047 |
)
|
| 1048 |
|
|
|
|
| 1049 |
add_lora_btn.click(
|
| 1050 |
fn=add_custom_lora,
|
| 1051 |
inputs=[lora_repo_id, lora_weight_name, lora_adapter_name, dynamic_loras_state],
|
|
@@ -1069,7 +771,6 @@ with gr.Blocks() as demo:
|
|
| 1069 |
outputs=[custom_prompt_display],
|
| 1070 |
)
|
| 1071 |
|
| 1072 |
-
# Canvas / fit / batch UI toggles
|
| 1073 |
canvas_mode.change(fn=on_canvas_mode_change, inputs=[canvas_mode],
|
| 1074 |
outputs=[custom_width, custom_height])
|
| 1075 |
canvas_fit_mode.change(fn=on_fit_mode_change, inputs=[canvas_fit_mode],
|
|
@@ -1077,12 +778,9 @@ with gr.Blocks() as demo:
|
|
| 1077 |
batch_vary.change(fn=on_batch_vary_change, inputs=[batch_vary],
|
| 1078 |
outputs=[sweep_min, sweep_max])
|
| 1079 |
|
| 1080 |
-
# Track which gallery item the user clicked, so Send→Base/Ref can use it
|
| 1081 |
output_gallery.select(fn=on_gallery_select, inputs=[output_gallery],
|
| 1082 |
outputs=[selected_output_state])
|
| 1083 |
|
| 1084 |
-
# The Generate-tab generator. .click() returns an event we keep so the
|
| 1085 |
-
# bulk Stop button can cancel mid-stream too if desired.
|
| 1086 |
run_event = run_button.click(
|
| 1087 |
fn=infer,
|
| 1088 |
inputs=[base_image, reference_images, prompt, lora_prompt_display, custom_prompt_display,
|
|
@@ -1094,8 +792,6 @@ with gr.Blocks() as demo:
|
|
| 1094 |
)
|
| 1095 |
|
| 1096 |
# ── Editor tab wiring ────────────────────────────────────────────────────
|
| 1097 |
-
# NOTE: do NOT add editor.upload(outputs=[editor]) — remounts the editor.
|
| 1098 |
-
|
| 1099 |
heic_uploader.upload(fn=load_heic_to_editor, inputs=[heic_uploader], outputs=[editor])
|
| 1100 |
|
| 1101 |
send_to_base_btn.click(fn=send_editor_to_base, inputs=[editor], outputs=[base_image]) \
|
|
@@ -1107,7 +803,6 @@ with gr.Blocks() as demo:
|
|
| 1107 |
.then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info]) \
|
| 1108 |
.then(fn=lambda: gr.Tabs(selected="tab_generate"), outputs=[main_tabs])
|
| 1109 |
|
| 1110 |
-
# Send-output buttons use the selected gallery item (or latest fallback)
|
| 1111 |
send_out_to_base_btn.click(
|
| 1112 |
fn=send_output_to_base,
|
| 1113 |
inputs=[selected_output_state, output_gallery],
|
|
@@ -1121,8 +816,6 @@ with gr.Blocks() as demo:
|
|
| 1121 |
).then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
|
| 1122 |
|
| 1123 |
# ── Bulk tab wiring ──────────────────────────────────────────────────────
|
| 1124 |
-
# Reuses Generate-tab settings as inputs verbatim — single source of truth,
|
| 1125 |
-
# no two-way state sync to keep in step.
|
| 1126 |
bulk_event = bulk_run_btn.click(
|
| 1127 |
fn=bulk_infer,
|
| 1128 |
inputs=[bulk_files,
|
|
@@ -1133,26 +826,19 @@ with gr.Blocks() as demo:
|
|
| 1133 |
outputs=[bulk_gallery, bulk_status, bulk_zip],
|
| 1134 |
)
|
| 1135 |
|
| 1136 |
-
# Stop cancels both running generators; the in-flight GPU call still
|
| 1137 |
-
# finishes (ZeroGPU can't be killed mid-step), but no further iterations
|
| 1138 |
-
# start. Any already-saved outputs remain in the gallery and on disk.
|
| 1139 |
bulk_stop_btn.click(fn=lambda: gr.Info("Stop requested — finishing current image."),
|
| 1140 |
cancels=[bulk_event, run_event])
|
| 1141 |
|
| 1142 |
# ── Depth / Pose tab wiring ──────────────────────────────────────────────
|
| 1143 |
|
| 1144 |
-
# Cache the last successfully-loaded source so re-renders after edits
|
| 1145 |
-
# don't need the user to keep the upload widget populated.
|
| 1146 |
ctrl_source.change(
|
| 1147 |
fn=lambda img: img, inputs=[ctrl_source], outputs=[pose_source_state],
|
| 1148 |
)
|
| 1149 |
|
| 1150 |
-
# Depth generation
|
| 1151 |
detect_depth_btn.click(
|
| 1152 |
fn=generate_depthmap, inputs=[ctrl_source], outputs=[depth_output],
|
| 1153 |
)
|
| 1154 |
|
| 1155 |
-
# Pose detection → fills state, dropdowns, both preview images.
|
| 1156 |
def _on_detect_pose(source):
|
| 1157 |
if source is None:
|
| 1158 |
raise gr.Error("Upload a source image first.")
|
|
@@ -1175,13 +861,12 @@ with gr.Blocks() as demo:
|
|
| 1175 |
outputs=[pose_keypoints_state, active_person_dd, active_joint_dd,
|
| 1176 |
pose_overlay, pose_clean],
|
| 1177 |
)
|
| 1178 |
-
reset_pose_btn.click(
|
| 1179 |
fn=_on_detect_pose, inputs=[ctrl_source],
|
| 1180 |
outputs=[pose_keypoints_state, active_person_dd, active_joint_dd,
|
| 1181 |
pose_overlay, pose_clean],
|
| 1182 |
)
|
| 1183 |
|
| 1184 |
-
# Insert a default standing-figure template centred in the source canvas.
|
| 1185 |
def _on_insert_blank(source):
|
| 1186 |
if source is None:
|
| 1187 |
raise gr.Error("Upload a source image first.")
|
|
@@ -1201,9 +886,6 @@ with gr.Blocks() as demo:
|
|
| 1201 |
pose_overlay, pose_clean],
|
| 1202 |
)
|
| 1203 |
|
| 1204 |
-
# ── Click-to-edit: the heart of the pose editor ──
|
| 1205 |
-
# gr.SelectData on a gr.Image gives .index = (x, y) in image pixels, even
|
| 1206 |
-
# when interactive=False, which is exactly what we need.
|
| 1207 |
def _on_overlay_click(evt: gr.SelectData, poses, source, person_label, joint_name):
|
| 1208 |
if not poses or source is None or evt is None or evt.index is None:
|
| 1209 |
return gr.update(), gr.update(), gr.update()
|
|
@@ -1227,8 +909,6 @@ with gr.Blocks() as demo:
|
|
| 1227 |
outputs=[pose_keypoints_state, pose_overlay, pose_clean],
|
| 1228 |
)
|
| 1229 |
|
| 1230 |
-
# Changing the active joint or person just re-renders the overlay so the
|
| 1231 |
-
# highlight ring follows — keypoints are not mutated.
|
| 1232 |
def _on_active_change(poses, source, person_label, joint_name):
|
| 1233 |
if not poses or source is None:
|
| 1234 |
return gr.update()
|
|
@@ -1249,7 +929,6 @@ with gr.Blocks() as demo:
|
|
| 1249 |
outputs=[pose_overlay],
|
| 1250 |
)
|
| 1251 |
|
| 1252 |
-
# Hide / clear
|
| 1253 |
def _on_hide_active(poses, source, person_label, joint_name):
|
| 1254 |
person_idx = parse_person_idx(person_label)
|
| 1255 |
joint_idx = joint_name_to_index(joint_name)
|
|
@@ -1283,8 +962,6 @@ with gr.Blocks() as demo:
|
|
| 1283 |
outputs=[pose_keypoints_state, pose_overlay, pose_clean],
|
| 1284 |
)
|
| 1285 |
|
| 1286 |
-
# Send → main tab. We reuse the existing base_image / reference_images
|
| 1287 |
-
# components so users land back on the Generate tab fully wired up.
|
| 1288 |
send_depth_ref_btn.click(
|
| 1289 |
fn=push_pil_to_reference, inputs=[depth_output, reference_images],
|
| 1290 |
outputs=[reference_images],
|
|
@@ -1307,7 +984,8 @@ with gr.Blocks() as demo:
|
|
| 1307 |
).then(fn=on_base_image_change, inputs=[base_image], outputs=[size_info]
|
| 1308 |
).then(fn=lambda: gr.Tabs(selected="tab_generate"), outputs=[main_tabs])
|
| 1309 |
|
|
|
|
| 1310 |
if __name__ == "__main__":
|
| 1311 |
-
# Gradio 6.0: theme and css go
|
| 1312 |
demo.queue().launch(css=css, theme=orange_red_theme,
|
| 1313 |
mcp_server=True, ssr_mode=False, show_error=True)
|
|
|
|
| 6 |
import uuid
|
| 7 |
import zipfile
|
| 8 |
import threading
|
|
|
|
| 9 |
|
| 10 |
import gradio as gr
|
|
|
|
| 11 |
import spaces
|
| 12 |
import torch
|
| 13 |
from PIL import Image
|
| 14 |
|
| 15 |
+
# ── Local modules — single source of truth for each concern ─────────────────
|
| 16 |
+
from config import (
|
| 17 |
+
MODEL_VARIANT,
|
| 18 |
+
MODEL_REPO,
|
| 19 |
+
MAX_SEED,
|
| 20 |
+
MAX_LORA_SLOTS,
|
| 21 |
+
ENABLE_LOGGING,
|
| 22 |
+
)
|
| 23 |
+
from ui_theme import orange_red_theme
|
| 24 |
+
from upscale import UPSCALE_MODELS, apply_realesrgan
|
| 25 |
+
from lora_registry import (
|
| 26 |
+
LORA_STYLES,
|
| 27 |
+
LOADED_ADAPTERS,
|
| 28 |
+
get_selectable_styles,
|
| 29 |
+
get_style_by_title,
|
| 30 |
+
update_weight_sliders,
|
| 31 |
+
add_custom_lora,
|
| 32 |
+
)
|
| 33 |
from logging_utils import log_inference
|
|
|
|
| 34 |
from image_utils import (
|
| 35 |
fix_orientation,
|
|
|
|
| 36 |
compute_canvas_dimensions,
|
| 37 |
fit_to_canvas,
|
| 38 |
on_base_image_change,
|
|
|
|
| 49 |
push_pil_to_base,
|
| 50 |
push_pil_to_reference,
|
| 51 |
)
|
|
|
|
| 52 |
from control_tools import (
|
| 53 |
generate_depthmap,
|
| 54 |
detect_pose,
|
|
|
|
| 64 |
OPENPOSE_KEYPOINT_NAMES,
|
| 65 |
)
|
| 66 |
|
| 67 |
+
# ── Model load ──────────────────────────────────────────────────────────────
|
| 68 |
+
# Pipeline class depends on MODEL_VARIANT and is the only thing here that
|
| 69 |
+
# can't live in config.py (config must stay torch/diffusers-free).
|
| 70 |
if MODEL_VARIANT == "9B-KV":
|
| 71 |
from diffusers import Flux2KleinKVPipeline as _PipeClass
|
|
|
|
| 72 |
else:
|
| 73 |
from diffusers import Flux2KleinPipeline as _PipeClass
|
|
|
|
|
|
|
| 74 |
|
| 75 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 76 |
|
| 77 |
+
print(f"Loading FLUX.2 Klein {MODEL_VARIANT} from {MODEL_REPO}...")
|
| 78 |
+
pipe = _PipeClass.from_pretrained(MODEL_REPO, torch_dtype=torch.bfloat16).to(device)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
print(f"Model loaded successfully: FLUX.2 Klein {MODEL_VARIANT}")
|
| 80 |
|
| 81 |
|
| 82 |
# ── UI helper callbacks ──────────────────────────────────────────────────────
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def on_canvas_mode_change(mode):
|
| 85 |
"""Custom W/H sliders only relevant when mode == Custom."""
|
| 86 |
is_custom = (mode == "Custom")
|
|
|
|
| 109 |
return item[0] if isinstance(item, (list, tuple)) else item
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
# ── Logging ──────────────────────────────────────────────────────────────────
|
| 113 |
|
| 114 |
def _spawn_log(pil_images, result_image, prompt, seed, steps, guidance_scale,
|
| 115 |
width, height, duration, success, error="",
|
| 116 |
lora_titles=None, lora_weights=None, upscale_factor="None",
|
| 117 |
lora_prompt_text=""):
|
| 118 |
+
"""Fire-and-forget logger. ENABLE_LOGGING is read once at import time in
|
| 119 |
+
config.py — there's no per-call env lookup. Used by single, batch and
|
| 120 |
+
bulk inference paths."""
|
| 121 |
+
if not ENABLE_LOGGING:
|
| 122 |
return
|
| 123 |
threading.Thread(
|
| 124 |
target=log_inference,
|
|
|
|
| 192 |
if upscale_factor and upscale_factor != "None":
|
| 193 |
gc.collect(); torch.cuda.synchronize(); torch.cuda.empty_cache()
|
| 194 |
try:
|
| 195 |
+
image = apply_realesrgan(image, upscale_factor, device)
|
| 196 |
except Exception as e:
|
| 197 |
gr.Warning(f"Upscaling failed, returning {width}×{height} result: {e}")
|
| 198 |
|
|
|
|
| 245 |
selected_titles = selected_titles or []
|
| 246 |
batch_count = max(1, int(batch_count))
|
| 247 |
|
|
|
|
|
|
|
| 248 |
base_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
|
| 249 |
seeds, weight_overrides = [], []
|
| 250 |
for i in range(batch_count):
|
|
|
|
| 323 |
into the bulk output gallery as soon as they complete. Outputs and a CSV
|
| 324 |
manifest are written to /tmp/bulk_<sid>/ with stable filenames, so a
|
| 325 |
ZeroGPU quota wall mid-run still leaves earlier outputs grabbable from
|
| 326 |
+
the gallery and from disk.
|
| 327 |
+
|
| 328 |
+
Logging: each image is logged individually via _spawn_log on success or
|
| 329 |
+
failure — same code path as single/batch, so bulk shows up in the dataset
|
| 330 |
+
with the same schema, just with extra bulk_index / bulk_total fields
|
| 331 |
+
embedded in the PNG metadata."""
|
| 332 |
if not input_files:
|
| 333 |
raise gr.Error("Upload at least one image first.")
|
| 334 |
|
|
|
|
| 365 |
*slider_values, progress=progress,
|
| 366 |
)
|
| 367 |
|
|
|
|
|
|
|
| 368 |
stem = os.path.splitext(fname)[0]
|
| 369 |
out_path = os.path.join(work_dir, f"{i:03d}_{stem}.png")
|
| 370 |
meta = _meta_for(prompt, used_seed, steps, guidance_scale, w, h, upscale_factor,
|
|
|
|
| 386 |
upscale_factor=upscale_factor, lora_prompt_text=lora_prompt_text or "")
|
| 387 |
|
| 388 |
status = f"✅ {succeeded}/{total} done ({failed} failed)"
|
| 389 |
+
yield results, status, gr.update()
|
| 390 |
except Exception as e:
|
| 391 |
failed += 1
|
| 392 |
duration = time.perf_counter() - t0
|
| 393 |
with open(manifest_path, "a", newline="") as f:
|
| 394 |
csv.writer(f).writerow([i, fname, "", "", "", "", False, str(e)[:300], f"{duration:.2f}"])
|
| 395 |
+
_spawn_log([], None, prompt, 0, steps, guidance_scale,
|
| 396 |
+
0, 0, duration, False, str(e),
|
| 397 |
+
lora_titles=log_titles, lora_weights=log_weights,
|
| 398 |
+
upscale_factor=upscale_factor, lora_prompt_text=lora_prompt_text or "")
|
| 399 |
yield results, f"⚠️ Image {i+1} failed: {e} | {succeeded} ok, {failed} failed", gr.update()
|
| 400 |
continue
|
| 401 |
|
|
|
|
| 402 |
zip_path = os.path.join(work_dir, "outputs.zip")
|
| 403 |
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_STORED) as zf:
|
| 404 |
for p in results:
|
|
|
|
| 408 |
yield results, f"🎉 Done — {succeeded}/{total} succeeded, {failed} failed", zip_path
|
| 409 |
|
| 410 |
|
| 411 |
+
# ── Custom prompt manager (session-local) ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
|
| 413 |
def add_custom_prompt(name, text, prompts_state, counter_state):
|
| 414 |
prompts = dict(prompts_state); counter = int(counter_state)
|
|
|
|
| 426 |
gr.update(choices=choices), gr.update(choices=choices),
|
| 427 |
gr.update(value="", interactive=True))
|
| 428 |
|
| 429 |
+
|
| 430 |
def delete_custom_prompt(name, currently_selected, prompts_state):
|
| 431 |
prompts = dict(prompts_state)
|
| 432 |
msg = f"🗑️ Deleted: '{name}'" if name and name in prompts else "Nothing to delete."
|
|
|
|
| 438 |
gr.update(choices=choices, value=new_sel),
|
| 439 |
gr.update(choices=choices, value=None))
|
| 440 |
|
| 441 |
+
|
| 442 |
def update_custom_prompt_display(selected_names, prompts_state):
|
| 443 |
if not selected_names:
|
| 444 |
return gr.update(value="", visible=False)
|
|
|
|
| 462 |
custom_prompts_state = gr.State({})
|
| 463 |
custom_prompt_counter_state = gr.State(0)
|
| 464 |
dynamic_loras_state = gr.State({})
|
| 465 |
+
selected_output_state = gr.State(None)
|
| 466 |
|
| 467 |
with gr.Column(elem_id="col-container"):
|
| 468 |
+
_logging_badge = "🟢 On" if ENABLE_LOGGING else "🔴 Off"
|
|
|
|
| 469 |
|
| 470 |
gr.Markdown("# **FLUX.2-Klein-LoRA-Studio**", elem_id="main-title")
|
| 471 |
gr.Markdown(
|
| 472 |
f"Apply one or more [LoRA](https://huggingface.co/models?other=base_model:adapter:black-forest-labs/FLUX.2-klein-9B) "
|
| 473 |
+
f"adapters using [FLUX.2-Klein-{MODEL_VARIANT}]({MODEL_REPO}). "
|
| 474 |
f"**Model:** `{MODEL_VARIANT}` · **Logging:** {_logging_badge}"
|
| 475 |
)
|
| 476 |
|
|
|
|
| 546 |
sweep_max = gr.Slider(label="Sweep max weight", minimum=0.0, maximum=2.0,
|
| 547 |
step=0.05, value=1.4, visible=False)
|
| 548 |
|
|
|
|
| 549 |
with gr.Column(scale=1):
|
|
|
|
|
|
|
|
|
|
| 550 |
output_gallery = gr.Gallery(
|
| 551 |
label="Output", type="filepath", columns=2, rows=2,
|
| 552 |
height=420, allow_preview=True, preview=True,
|
|
|
|
| 564 |
"Civitai / ComfyUI readable).*"
|
| 565 |
)
|
| 566 |
|
|
|
|
| 567 |
gr.Markdown("### 🎨 Select LoRA(s)")
|
| 568 |
lora_selector = gr.CheckboxGroup(
|
| 569 |
choices=[s["title"] for s in get_selectable_styles({})],
|
|
|
|
| 673 |
"Reference image."
|
| 674 |
)
|
| 675 |
|
| 676 |
+
pose_source_state = gr.State(None)
|
| 677 |
+
pose_keypoints_state = gr.State([])
|
|
|
|
|
|
|
| 678 |
|
| 679 |
with gr.Row():
|
| 680 |
with gr.Column(scale=1):
|
|
|
|
| 704 |
|
| 705 |
with gr.Row():
|
| 706 |
with gr.Column(scale=1):
|
|
|
|
|
|
|
| 707 |
pose_overlay = gr.Image(
|
| 708 |
label="Editor — click to place active joint",
|
| 709 |
type="pil", interactive=False, height=420, format="png",
|
|
|
|
| 733 |
send_pose_ref_btn = gr.Button("→ Send pose to Reference",
|
| 734 |
variant="primary")
|
| 735 |
send_pose_base_btn = gr.Button("→ Send pose to Base")
|
| 736 |
+
|
| 737 |
# ── Event wiring ─────────────────────────────────────────────────────────
|
| 738 |
|
|
|
|
| 739 |
base_image.upload(fn=reencode_upload, inputs=[base_image], outputs=[base_image])
|
|
|
|
| 740 |
base_image.change(fn=on_base_image_change, inputs=[base_image], outputs=[size_info])
|
| 741 |
reference_images.change(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
|
| 742 |
|
| 743 |
+
# update_weight_sliders is the one imported from lora_registry now.
|
| 744 |
lora_selector.change(
|
| 745 |
fn=update_weight_sliders,
|
| 746 |
inputs=[lora_selector, dynamic_loras_state],
|
| 747 |
outputs=weight_sliders + [lora_prompt_display],
|
| 748 |
)
|
| 749 |
|
| 750 |
+
# add_custom_lora is also imported from lora_registry.
|
| 751 |
add_lora_btn.click(
|
| 752 |
fn=add_custom_lora,
|
| 753 |
inputs=[lora_repo_id, lora_weight_name, lora_adapter_name, dynamic_loras_state],
|
|
|
|
| 771 |
outputs=[custom_prompt_display],
|
| 772 |
)
|
| 773 |
|
|
|
|
| 774 |
canvas_mode.change(fn=on_canvas_mode_change, inputs=[canvas_mode],
|
| 775 |
outputs=[custom_width, custom_height])
|
| 776 |
canvas_fit_mode.change(fn=on_fit_mode_change, inputs=[canvas_fit_mode],
|
|
|
|
| 778 |
batch_vary.change(fn=on_batch_vary_change, inputs=[batch_vary],
|
| 779 |
outputs=[sweep_min, sweep_max])
|
| 780 |
|
|
|
|
| 781 |
output_gallery.select(fn=on_gallery_select, inputs=[output_gallery],
|
| 782 |
outputs=[selected_output_state])
|
| 783 |
|
|
|
|
|
|
|
| 784 |
run_event = run_button.click(
|
| 785 |
fn=infer,
|
| 786 |
inputs=[base_image, reference_images, prompt, lora_prompt_display, custom_prompt_display,
|
|
|
|
| 792 |
)
|
| 793 |
|
| 794 |
# ── Editor tab wiring ────────────────────────────────────────────────────
|
|
|
|
|
|
|
| 795 |
heic_uploader.upload(fn=load_heic_to_editor, inputs=[heic_uploader], outputs=[editor])
|
| 796 |
|
| 797 |
send_to_base_btn.click(fn=send_editor_to_base, inputs=[editor], outputs=[base_image]) \
|
|
|
|
| 803 |
.then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info]) \
|
| 804 |
.then(fn=lambda: gr.Tabs(selected="tab_generate"), outputs=[main_tabs])
|
| 805 |
|
|
|
|
| 806 |
send_out_to_base_btn.click(
|
| 807 |
fn=send_output_to_base,
|
| 808 |
inputs=[selected_output_state, output_gallery],
|
|
|
|
| 816 |
).then(fn=on_reference_change, inputs=[reference_images], outputs=[reference_info])
|
| 817 |
|
| 818 |
# ── Bulk tab wiring ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
| 819 |
bulk_event = bulk_run_btn.click(
|
| 820 |
fn=bulk_infer,
|
| 821 |
inputs=[bulk_files,
|
|
|
|
| 826 |
outputs=[bulk_gallery, bulk_status, bulk_zip],
|
| 827 |
)
|
| 828 |
|
|
|
|
|
|
|
|
|
|
| 829 |
bulk_stop_btn.click(fn=lambda: gr.Info("Stop requested — finishing current image."),
|
| 830 |
cancels=[bulk_event, run_event])
|
| 831 |
|
| 832 |
# ── Depth / Pose tab wiring ──────────────────────────────────────────────
|
| 833 |
|
|
|
|
|
|
|
| 834 |
ctrl_source.change(
|
| 835 |
fn=lambda img: img, inputs=[ctrl_source], outputs=[pose_source_state],
|
| 836 |
)
|
| 837 |
|
|
|
|
| 838 |
detect_depth_btn.click(
|
| 839 |
fn=generate_depthmap, inputs=[ctrl_source], outputs=[depth_output],
|
| 840 |
)
|
| 841 |
|
|
|
|
| 842 |
def _on_detect_pose(source):
|
| 843 |
if source is None:
|
| 844 |
raise gr.Error("Upload a source image first.")
|
|
|
|
| 861 |
outputs=[pose_keypoints_state, active_person_dd, active_joint_dd,
|
| 862 |
pose_overlay, pose_clean],
|
| 863 |
)
|
| 864 |
+
reset_pose_btn.click(
|
| 865 |
fn=_on_detect_pose, inputs=[ctrl_source],
|
| 866 |
outputs=[pose_keypoints_state, active_person_dd, active_joint_dd,
|
| 867 |
pose_overlay, pose_clean],
|
| 868 |
)
|
| 869 |
|
|
|
|
| 870 |
def _on_insert_blank(source):
|
| 871 |
if source is None:
|
| 872 |
raise gr.Error("Upload a source image first.")
|
|
|
|
| 886 |
pose_overlay, pose_clean],
|
| 887 |
)
|
| 888 |
|
|
|
|
|
|
|
|
|
|
| 889 |
def _on_overlay_click(evt: gr.SelectData, poses, source, person_label, joint_name):
|
| 890 |
if not poses or source is None or evt is None or evt.index is None:
|
| 891 |
return gr.update(), gr.update(), gr.update()
|
|
|
|
| 909 |
outputs=[pose_keypoints_state, pose_overlay, pose_clean],
|
| 910 |
)
|
| 911 |
|
|
|
|
|
|
|
| 912 |
def _on_active_change(poses, source, person_label, joint_name):
|
| 913 |
if not poses or source is None:
|
| 914 |
return gr.update()
|
|
|
|
| 929 |
outputs=[pose_overlay],
|
| 930 |
)
|
| 931 |
|
|
|
|
| 932 |
def _on_hide_active(poses, source, person_label, joint_name):
|
| 933 |
person_idx = parse_person_idx(person_label)
|
| 934 |
joint_idx = joint_name_to_index(joint_name)
|
|
|
|
| 962 |
outputs=[pose_keypoints_state, pose_overlay, pose_clean],
|
| 963 |
)
|
| 964 |
|
|
|
|
|
|
|
| 965 |
send_depth_ref_btn.click(
|
| 966 |
fn=push_pil_to_reference, inputs=[depth_output, reference_images],
|
| 967 |
outputs=[reference_images],
|
|
|
|
| 984 |
).then(fn=on_base_image_change, inputs=[base_image], outputs=[size_info]
|
| 985 |
).then(fn=lambda: gr.Tabs(selected="tab_generate"), outputs=[main_tabs])
|
| 986 |
|
| 987 |
+
|
| 988 |
if __name__ == "__main__":
|
| 989 |
+
# Gradio 6.0: theme and css go on launch(), not Blocks()
|
| 990 |
demo.queue().launch(css=css, theme=orange_red_theme,
|
| 991 |
mcp_server=True, ssr_mode=False, show_error=True)
|
lora_registry.py
CHANGED
|
@@ -33,8 +33,7 @@ FROM PICTURE 2 (strictly preserve identity):
|
|
| 33 |
- Skin: texture, tone, complexion
|
| 34 |
The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k."""
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
MAX_LORA_SLOTS = 6
|
| 38 |
|
| 39 |
LORA_STYLES = [
|
| 40 |
{
|
|
@@ -85,7 +84,7 @@ LORA_STYLES = [
|
|
| 85 |
"weights": "Flux2-Klein-Image-RestoreV1.safetensors",
|
| 86 |
"default_prompt": "restore the image quality, remove any compression artefacts, remove any haze and soft edges, enrich the original with new intricate detail in all textures and surfaces creating a professional photorealistic photograph with natural lighting and skin texture.",
|
| 87 |
"default_weight": 1.0,
|
| 88 |
-
},
|
| 89 |
{
|
| 90 |
"title": "High Resolution",
|
| 91 |
"adapter_name": "High Resolution",
|
|
@@ -93,7 +92,7 @@ LORA_STYLES = [
|
|
| 93 |
"weights": "HighResolution9B.safetensors",
|
| 94 |
"default_prompt": "High Resolution",
|
| 95 |
"default_weight": 1.0,
|
| 96 |
-
},
|
| 97 |
{
|
| 98 |
"title": "InstaPic",
|
| 99 |
"adapter_name": "InstaPic V3",
|
|
@@ -101,7 +100,7 @@ LORA_STYLES = [
|
|
| 101 |
"weights": "InstaPic V3.safetensors",
|
| 102 |
"default_prompt": "instapic",
|
| 103 |
"default_weight": 1.0,
|
| 104 |
-
},
|
| 105 |
{
|
| 106 |
"title": "Realistic Nudes",
|
| 107 |
"adapter_name": "Realistic Nudes",
|
|
@@ -109,7 +108,7 @@ LORA_STYLES = [
|
|
| 109 |
"weights": "realistic_nudes_klein_v3.safetensors",
|
| 110 |
"default_prompt": None,
|
| 111 |
"default_weight": 1.0,
|
| 112 |
-
},
|
| 113 |
{
|
| 114 |
"title": "Perky Pointy Puffy Breasts",
|
| 115 |
"adapter_name": "Perky Pointy Puffy Breasts",
|
|
@@ -117,7 +116,7 @@ LORA_STYLES = [
|
|
| 117 |
"weights": "PerkyPointyPuffy_v1.1_small_pointy_breasts_large_puffy_nipples.safetensors",
|
| 118 |
"default_prompt": "Small pointy breasts with large puffy nipples",
|
| 119 |
"default_weight": 1.0,
|
| 120 |
-
},
|
| 121 |
{
|
| 122 |
"title": "Flat Chested",
|
| 123 |
"adapter_name": "Flat Chested",
|
|
@@ -125,7 +124,7 @@ LORA_STYLES = [
|
|
| 125 |
"weights": "Flux2-Klein-9b-FlatChested-v1.safetensors",
|
| 126 |
"default_prompt": "flat chested",
|
| 127 |
"default_weight": 1.5,
|
| 128 |
-
},
|
| 129 |
{
|
| 130 |
"title": "Controllight",
|
| 131 |
"adapter_name": "Controllight",
|
|
@@ -133,7 +132,7 @@ LORA_STYLES = [
|
|
| 133 |
"weights": "controllight.safetensors",
|
| 134 |
"default_prompt": None,
|
| 135 |
"default_weight": 1.0,
|
| 136 |
-
},
|
| 137 |
{
|
| 138 |
"title": "RefControl - Depth",
|
| 139 |
"adapter_name": "RefConDep",
|
|
@@ -141,7 +140,7 @@ LORA_STYLES = [
|
|
| 141 |
"weights": "flux2_klein_9b_refcontrol_depth.safetensors",
|
| 142 |
"default_prompt": "refcontrol",
|
| 143 |
"default_weight": 1.0,
|
| 144 |
-
},
|
| 145 |
{
|
| 146 |
"title": "RefControl - Pose",
|
| 147 |
"adapter_name": "RefConPos",
|
|
@@ -149,11 +148,10 @@ LORA_STYLES = [
|
|
| 149 |
"weights": "refcontrol_v2_poses.safetensors",
|
| 150 |
"default_prompt": "apply pose from image 1 with reference from image 2",
|
| 151 |
"default_weight": 1.0,
|
| 152 |
-
},
|
| 153 |
]
|
| 154 |
|
| 155 |
|
| 156 |
-
|
| 157 |
# LOADED_ADAPTERS is the only piece of LoRA state that's legitimately global:
|
| 158 |
# it just tracks which adapter names have been loaded onto the shared `pipe`
|
| 159 |
# at least once, so we don't re-download/re-attach weights on every call.
|
|
@@ -218,7 +216,6 @@ def update_weight_sliders(selected_titles, dynamic_loras):
|
|
| 218 |
else:
|
| 219 |
slider_updates.append(gr.update(visible=False, value=1.0))
|
| 220 |
|
| 221 |
-
# Collect default prompts from ALL selected LoRAs (not just one)
|
| 222 |
lora_prompts = [s.get("default_prompt") for s in selected_styles if s.get("default_prompt")]
|
| 223 |
if lora_prompts:
|
| 224 |
combined = "\n\n".join(lora_prompts)
|
|
|
|
| 33 |
- Skin: texture, tone, complexion
|
| 34 |
The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k."""
|
| 35 |
|
| 36 |
+
# NOTE: MAX_LORA_SLOTS is imported from config.py — do NOT re-declare it here.
|
|
|
|
| 37 |
|
| 38 |
LORA_STYLES = [
|
| 39 |
{
|
|
|
|
| 84 |
"weights": "Flux2-Klein-Image-RestoreV1.safetensors",
|
| 85 |
"default_prompt": "restore the image quality, remove any compression artefacts, remove any haze and soft edges, enrich the original with new intricate detail in all textures and surfaces creating a professional photorealistic photograph with natural lighting and skin texture.",
|
| 86 |
"default_weight": 1.0,
|
| 87 |
+
},
|
| 88 |
{
|
| 89 |
"title": "High Resolution",
|
| 90 |
"adapter_name": "High Resolution",
|
|
|
|
| 92 |
"weights": "HighResolution9B.safetensors",
|
| 93 |
"default_prompt": "High Resolution",
|
| 94 |
"default_weight": 1.0,
|
| 95 |
+
},
|
| 96 |
{
|
| 97 |
"title": "InstaPic",
|
| 98 |
"adapter_name": "InstaPic V3",
|
|
|
|
| 100 |
"weights": "InstaPic V3.safetensors",
|
| 101 |
"default_prompt": "instapic",
|
| 102 |
"default_weight": 1.0,
|
| 103 |
+
},
|
| 104 |
{
|
| 105 |
"title": "Realistic Nudes",
|
| 106 |
"adapter_name": "Realistic Nudes",
|
|
|
|
| 108 |
"weights": "realistic_nudes_klein_v3.safetensors",
|
| 109 |
"default_prompt": None,
|
| 110 |
"default_weight": 1.0,
|
| 111 |
+
},
|
| 112 |
{
|
| 113 |
"title": "Perky Pointy Puffy Breasts",
|
| 114 |
"adapter_name": "Perky Pointy Puffy Breasts",
|
|
|
|
| 116 |
"weights": "PerkyPointyPuffy_v1.1_small_pointy_breasts_large_puffy_nipples.safetensors",
|
| 117 |
"default_prompt": "Small pointy breasts with large puffy nipples",
|
| 118 |
"default_weight": 1.0,
|
| 119 |
+
},
|
| 120 |
{
|
| 121 |
"title": "Flat Chested",
|
| 122 |
"adapter_name": "Flat Chested",
|
|
|
|
| 124 |
"weights": "Flux2-Klein-9b-FlatChested-v1.safetensors",
|
| 125 |
"default_prompt": "flat chested",
|
| 126 |
"default_weight": 1.5,
|
| 127 |
+
},
|
| 128 |
{
|
| 129 |
"title": "Controllight",
|
| 130 |
"adapter_name": "Controllight",
|
|
|
|
| 132 |
"weights": "controllight.safetensors",
|
| 133 |
"default_prompt": None,
|
| 134 |
"default_weight": 1.0,
|
| 135 |
+
},
|
| 136 |
{
|
| 137 |
"title": "RefControl - Depth",
|
| 138 |
"adapter_name": "RefConDep",
|
|
|
|
| 140 |
"weights": "flux2_klein_9b_refcontrol_depth.safetensors",
|
| 141 |
"default_prompt": "refcontrol",
|
| 142 |
"default_weight": 1.0,
|
| 143 |
+
},
|
| 144 |
{
|
| 145 |
"title": "RefControl - Pose",
|
| 146 |
"adapter_name": "RefConPos",
|
|
|
|
| 148 |
"weights": "refcontrol_v2_poses.safetensors",
|
| 149 |
"default_prompt": "apply pose from image 1 with reference from image 2",
|
| 150 |
"default_weight": 1.0,
|
| 151 |
+
},
|
| 152 |
]
|
| 153 |
|
| 154 |
|
|
|
|
| 155 |
# LOADED_ADAPTERS is the only piece of LoRA state that's legitimately global:
|
| 156 |
# it just tracks which adapter names have been loaded onto the shared `pipe`
|
| 157 |
# at least once, so we don't re-download/re-attach weights on every call.
|
|
|
|
| 216 |
else:
|
| 217 |
slider_updates.append(gr.update(visible=False, value=1.0))
|
| 218 |
|
|
|
|
| 219 |
lora_prompts = [s.get("default_prompt") for s in selected_styles if s.get("default_prompt")]
|
| 220 |
if lora_prompts:
|
| 221 |
combined = "\n\n".join(lora_prompts)
|