Surae007 commited on
Commit
11915ae
·
verified ·
1 Parent(s): 9a6ca47
Files changed (1) hide show
  1. app.py +490 -0
app.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, io, time, json, math
2
+ from typing import List, Dict, Optional, Tuple
3
+
4
+ import gradio as gr
5
+ import numpy as np
6
+ from PIL import Image, ImageOps
7
+
8
+ import torch
9
+ from diffusers import (
10
+ StableDiffusionXLPipeline,
11
+ StableDiffusionXLImg2ImgPipeline,
12
+ StableDiffusionXLInpaintPipeline,
13
+ StableDiffusionXLControlNetPipeline,
14
+ ControlNetModel,
15
+ StableDiffusionUpscalePipeline,
16
+ DPMSolverMultistepScheduler, EulerDiscreteScheduler,
17
+ EulerAncestralDiscreteScheduler, HeunDiscreteScheduler,
18
+ )
19
+
20
+ # ---------- Optional deps ----------
21
+ try:
22
+ from rembg import remove as rembg_remove
23
+ except Exception:
24
+ rembg_remove = None
25
+
26
+ # face restore
27
+ try:
28
+ from gfpgan import GFPGANer
29
+ _HAS_GFP = True
30
+ except Exception:
31
+ _HAS_GFP = False
32
+
33
+ # realesrgan (fallback upscaler)
34
+ try:
35
+ from realesrgan import RealESRGAN
36
+ _HAS_REALESRGAN = True
37
+ except Exception:
38
+ _HAS_REALESRGAN = False
39
+
40
+ device = "cuda" if torch.cuda.is_available() else "cpu"
41
+ dtype = torch.float16 if device == "cuda" else torch.float32
42
+
43
+ # ---------------- Registry (เปลี่ยน/เพิ่มได้เอง) ----------------
44
+ MODELS: List[Tuple[str,str,str]] = [
45
+ # (id, label, note)
46
+ ("stabilityai/stable-diffusion-xl-base-1.0", "SDXL Base 1.0", "เอนกประสงค์ สมดุล"),
47
+ ("stabilityai/stable-diffusion-xl-refiner-1.0","SDXL Refiner", "เก็บรายละเอียด (pass 2)"),
48
+ ("SG161222/RealVisXL_V4.0", "RealVis XL v4", "โฟโต้เรียล คน/สินค้าเนียน"),
49
+ ("Lykon/dreamshaper-xl-v2", "DreamShaper XL","แฟนตาซี–เรียลลิสติกหลากสไตล์"),
50
+ ("RunDiffusion/Juggernaut-XL", "Juggernaut XL", "คอนทราสต์แรง รายละเอียดหนัก"),
51
+ ("emilianJR/epiCRealismXL", "EpicRealism XL","แฟชั่น/พอร์เทรตคอนทราสต์ดี"),
52
+ ("black-forest-labs/FLUX.1-dev", "FLUX.1-dev", "สมัยใหม่/คุมสไตล์ดี (ไม่ใช่ SDXL)"),
53
+ ("stabilityai/sd-turbo", "SD-Turbo", "ไวมาก เหมาะกับร่างไอเดีย"),
54
+ ("stabilityai/stable-diffusion-2-1", "SD 2.1", "แลนด์สเคป/องค์ประกอบกว้าง"),
55
+ ("runwayml/stable-diffusion-v1-5", "SD 1.5", "คลาสสิก/ทรัพยากรเยอะ"),
56
+ ("timbrooks/instruct-pix2pix", "Instruct-Pix2Pix","แก้ภาพตามคำสั่ง (Img2Img)"),
57
+ ]
58
+
59
+ LORAS: List[Tuple[str,str,str]] = [
60
+ # หมายเหตุ: รายชื่อ LoRA แพร่หลายและเปลี่ยนเร็ว; ถ้าโหลดไม่ได้ โปรแกรมจะไม่ล้ม
61
+ ("ByteDance/SDXL-Lightning", "SDXL-Lightning", "สปีดเร็ว (LoRA)"),
62
+ ("ostris/epicrealism-xl-lora", "EpicrealismXL-LoRA","โทนเรียลลิสติก"),
63
+ ("XLabs-AI/flux-prompt-lora", "FLUX Prompt LoRA", "ปรับ prompt style (FLUX)"),
64
+ ("XLabs-AI/realvisxl-v4-lora", "RealVisXL LoRA", "พอร์เทรต/สินค้า"),
65
+ ("alpha-diffusion/sdxl-anime-lora", "Anime-Style XL", "อนิเม/เส้นใส"),
66
+ ("alpha-diffusion/sdxl-cinematic-lora", "Cinematic-Drama", "แสงเงาแบบหนัง"),
67
+ ("alpha-diffusion/sdxl-watercolor-lora", "Watercolor-Soft", "สีน้ำ/พาสเทล"),
68
+ ("alpha-diffusion/sdxl-fashion-lora", "Fashion-Editorial","แฟชั่น/กองถ่าย"),
69
+ ("alpha-diffusion/sdxl-product-lora", "Product-Studio", "สินค้า/แสงสตูดิโอ"),
70
+ ("alpha-diffusion/sdxl-interior-lora", "Interior-Archi", "ห้อง/สถาปัตย์"),
71
+ ("alpha-diffusion/sdxl-food-lora", "Food-Tasty", "อาหารฉ่ำ/เงางาม"),
72
+ ]
73
+
74
+ CONTROLNETS: List[Tuple[str,str,str,str]] = [
75
+ # (id, label, note, key)
76
+ ("diffusers/controlnet-canny-sdxl-1.0", "Canny", "คุมเส้นขอบ", "canny"),
77
+ ("diffusers/controlnet-openpose-sdxl-1.0", "OpenPose", "คุมท่าทางคน", "pose"),
78
+ ("diffusers/controlnet-depth-sdxl-1.0", "Depth", "คุมมุมมอง/ระยะลึก", "depth"),
79
+ ("diffusers/controlnet-softedge-sdxl-1.0", "SoftEdge", "เส้นนุ่ม/ลดแตก", "softedge"),
80
+ ("diffusers/controlnet-lineart-sdxl-1.0", "Lineart", "เส้นร่าง/การ์ตูน", "lineart"),
81
+ ("diffusers/controlnet-anime-lineart-sdxl-1.0","Anime Lineart","เส้นอนิเ��", "anime_lineart"),
82
+ ("diffusers/controlnet-normal-sdxl-1.0", "Normal", "ทิศทางพื้นผิว", "normal"),
83
+ ("diffusers/controlnet-mlsd-sdxl-1.0", "MLSD", "เส้นตรง/สถาปัตย์", "mlsd"),
84
+ ("diffusers/controlnet-scribble-sdxl-1.0", "Scribble", "สเก็ตช์หยาบ→จริง", "scribble"),
85
+ ("diffusers/controlnet-seg-sdxl-1.0", "Segmentation", "แบ่งส่วน/สี", "seg"),
86
+ ("diffusers/controlnet-tile-sdxl-1.0", "Tile", "อัปสเกลแบบกระเบื้อง", "tile"),
87
+ ]
88
+
89
+ PRESETS = {
90
+ "Cinematic": ", cinematic lighting, 50mm, bokeh, film grain, high dynamic range",
91
+ "Studio": ", studio photo, softbox lighting, sharp focus, high detail",
92
+ "Product": ", product photography, seamless background, diffused light, reflections",
93
+ "Anime": ", anime style, clean lineart, vibrant colors, high quality",
94
+ }
95
+ NEG_DEFAULT = "lowres, blurry, bad anatomy, extra fingers, watermark, jpeg artifacts, text"
96
+
97
+ SCHEDULERS = {
98
+ "DPM-Solver (Karras)": DPMSolverMultistepScheduler,
99
+ "Euler": EulerDiscreteScheduler,
100
+ "Euler a": EulerAncestralDiscreteScheduler,
101
+ "Heun": HeunDiscreteScheduler,
102
+ }
103
+
104
+ # ---------------- Cache & helpers ----------------
105
+ PIPE_CACHE: Dict[str, object] = {}
106
+ CONTROL_CACHE: Dict[str, ControlNetModel] = {}
107
+ UPSCALE_PIPE: Optional[StableDiffusionUpscalePipeline] = None
108
+ GFP: Optional[GFPGANer] = None
109
+ REALSR: Optional[RealESRGAN] = None
110
+
111
+ def set_sched(pipe, name: str):
112
+ cls = SCHEDULERS.get(name, DPMSolverMultistepScheduler)
113
+ pipe.scheduler = cls.from_config(pipe.scheduler.config)
114
+
115
+ def seed_gen(sd: int):
116
+ if sd is None or sd < 0: return None
117
+ g = torch.Generator(device=device if device=="cuda" else "cpu")
118
+ g.manual_seed(int(sd))
119
+ return g
120
+
121
+ def prep_pipe(model_id: str, control_ids: List[str]):
122
+ key = f"{model_id}|{'-'.join(control_ids) if control_ids else 'none'}"
123
+ if key in PIPE_CACHE: return PIPE_CACHE[key]
124
+
125
+ if control_ids:
126
+ cns = []
127
+ for cid in control_ids:
128
+ if cid not in CONTROL_CACHE:
129
+ CONTROL_CACHE[cid] = ControlNetModel.from_pretrained(cid, torch_dtype=dtype, use_safetensors=True)
130
+ cns.append(CONTROL_CACHE[cid])
131
+ pipe = StableDiffusionXLControlNetPipeline.from_pretrained(model_id, controlnet=cns, torch_dtype=dtype, use_safetensors=True)
132
+ else:
133
+ pipe = StableDiffusionXLPipeline.from_pretrained(model_id, torch_dtype=dtype, use_safetensors=True)
134
+
135
+ if device == "cuda":
136
+ pipe.to("cuda")
137
+ pipe.enable_vae_tiling(); pipe.enable_vae_slicing()
138
+ try: pipe.enable_xformers_memory_efficient_attention()
139
+ except Exception: pass
140
+ else:
141
+ pipe.to("cpu"); pipe.enable_attention_slicing()
142
+
143
+ PIPE_CACHE[key] = pipe
144
+ return pipe
145
+
146
+ def apply_loras(pipe, ids: List[str], scales: List[float]):
147
+ for i, rid in enumerate([x for x in ids if x]):
148
+ try:
149
+ pipe.load_lora_weights(rid)
150
+ try:
151
+ sc = scales[i] if i < len(scales) else 0.7
152
+ pipe.fuse_lora(lora_scale=float(sc))
153
+ except Exception: pass
154
+ except Exception as e:
155
+ print(f"[LoRA] load failed {rid}: {e}")
156
+
157
+ def to_png_info(meta: dict) -> str:
158
+ return json.dumps(meta, ensure_ascii=False, indent=2)
159
+
160
+ # ---------------- Optional Post-process ----------------
161
+ def ensure_upscalers():
162
+ global UPSCALE_PIPE, GFP, REALSR
163
+ if UPSCALE_PIPE is None:
164
+ try:
165
+ UPSCALE_PIPE = StableDiffusionUpscalePipeline.from_pretrained(
166
+ "stabilityai/stable-diffusion-x4-upscaler",
167
+ torch_dtype=torch.float16 if device=="cuda" else torch.float32,
168
+ use_safetensors=True
169
+ ).to(device)
170
+ except Exception as e:
171
+ print("[Upscaler] SD x4 not available:", e)
172
+
173
+ if _HAS_GFP and GFP is None:
174
+ try:
175
+ GFP = GFPGANer(model_path=None, upscale=1, arch="clean", channel_multiplier=2)
176
+ except Exception as e:
177
+ print("[GFPGAN] init failed:", e)
178
+
179
+ if _HAS_REALESRGAN and REALSR is None and device == "cuda":
180
+ try:
181
+ REALSR = RealESRGAN(torch.device("cuda"), scale=4)
182
+ REALSR.load_weights("weights/RealESRGAN_x4plus.pth") # ใช้ได้เมื่อมีไฟล์ (ถ้าไม่มีจะข้าม)
183
+ except Exception as e:
184
+ print("[RealESRGAN] init failed:", e)
185
+
186
+ def post_process(img: Image.Image, do_upscale: bool, do_face: bool, do_rembg: bool):
187
+ ensure_upscalers()
188
+ out = img
189
+
190
+ # Upscale priority: RealESRGAN > SD x4 > none
191
+ if do_upscale:
192
+ try:
193
+ if REALSR is not None:
194
+ out = Image.fromarray(REALSR.predict(np.array(out)))
195
+ elif UPSCALE_PIPE is not None:
196
+ if device == "cuda":
197
+ with torch.autocast("cuda"):
198
+ out = UPSCALE_PIPE(prompt="", image=out).images[0]
199
+ else:
200
+ out = UPSCALE_PIPE(prompt="", image=out).images[0]
201
+ except Exception as e:
202
+ print("[Upscale] skipped:", e)
203
+
204
+ if do_face and _HAS_GFP and GFP is not None:
205
+ try:
206
+ _, _, out = GFP.enhance(np.array(out), has_aligned=False, only_center_face=False, paste_back=True)
207
+ out = Image.fromarray(out)
208
+ except Exception as e:
209
+ print("[GFPGAN] skipped:", e)
210
+
211
+ if do_rembg and rembg_remove is not None:
212
+ try:
213
+ out = Image.open(io.BytesIO(rembg_remove(np.array(out))))
214
+ except Exception as e:
215
+ print("[rembg] skipped:", e)
216
+
217
+ return out
218
+
219
+ # ---------------- Core generate ----------------
220
+ def run_txt2img(model_id, custom_model, prompt, preset, negative,
221
+ steps, cfg, width, height, scheduler_name, seed,
222
+ lora_list, lora_custom_csv, lora_s1, lora_s2, lora_s3,
223
+ ctrl_selected, ctrl_images, use_refiner, refine_strength,
224
+ do_upscale, do_face, do_rembg):
225
+ if not prompt.strip(): raise gr.Error("กรุณากรอก prompt")
226
+ model = (custom_model.strip() or model_id).strip()
227
+ if preset in PRESETS: prompt = prompt + PRESETS[preset]
228
+ if not negative.strip(): negative = NEG_DEFAULT
229
+
230
+ # Collect control images
231
+ cond_imgs, ctrl_ids = [], []
232
+ for (cid, label, note, key) in CONTROLNETS:
233
+ if label in ctrl_selected and key in ctrl_images and ctrl_images[key] is not None:
234
+ ctrl_ids.append(cid); cond_imgs.append(ctrl_images[key])
235
+
236
+ pipe = prep_pipe(model, ctrl_ids)
237
+ set_sched(pipe, scheduler_name)
238
+
239
+ loras = []
240
+ if lora_list: loras += lora_list
241
+ if lora_custom_csv.strip():
242
+ loras += [x.strip() for x in lora_custom_csv.split(",") if x.strip()]
243
+ apply_loras(pipe, loras, [lora_s1, lora_s2, lora_s3])
244
+
245
+ width = int(max(512, min(1024, width)))
246
+ height = int(max(512, min(1024, height)))
247
+ gen = seed_gen(seed)
248
+
249
+ if device == "cuda":
250
+ with torch.autocast("cuda"):
251
+ if ctrl_ids:
252
+ image = pipe(prompt=prompt, negative_prompt=negative,
253
+ width=width, height=height,
254
+ num_inference_steps=int(steps), guidance_scale=float(cfg),
255
+ controlnet_conditioning_image=cond_imgs if len(cond_imgs)>1 else cond_imgs[0],
256
+ generator=gen).images[0]
257
+ else:
258
+ image = pipe(prompt=prompt, negative_prompt=negative,
259
+ width=width, height=height,
260
+ num_inference_steps=int(steps), guidance_scale=float(cfg),
261
+ generator=gen).images[0]
262
+ else:
263
+ if ctrl_ids:
264
+ image = pipe(prompt=prompt, negative_prompt=negative,
265
+ width=width, height=height,
266
+ num_inference_steps=int(steps), guidance_scale=float(cfg),
267
+ controlnet_conditioning_image=cond_imgs if len(cond_imgs)>1 else cond_imgs[0],
268
+ generator=gen).images[0]
269
+ else:
270
+ image = pipe(prompt=prompt, negative_prompt=negative,
271
+ width=width, height=height,
272
+ num_inference_steps=int(steps), guidance_scale=float(cfg),
273
+ generator=gen).images[0]
274
+
275
+ # Refiner (GPU)
276
+ if use_refiner and device == "cuda":
277
+ try:
278
+ ref = StableDiffusionXLImg2ImgPipeline.from_pretrained(
279
+ "stabilityai/stable-diffusion-xl-refiner-1.0",
280
+ torch_dtype=torch.float16, use_safetensors=True
281
+ ).to("cuda")
282
+ set_sched(ref, scheduler_name)
283
+ with torch.autocast("cuda"):
284
+ image = ref(prompt=prompt, negative_prompt=negative,
285
+ image=image, strength=float(refine_strength),
286
+ num_inference_steps=max(10, int(steps)//2),
287
+ guidance_scale=float(cfg), generator=gen).images[0]
288
+ except Exception as e:
289
+ print("[Refiner] skipped:", e)
290
+
291
+ # Post-process
292
+ image = post_process(image, do_upscale, do_face, do_rembg)
293
+
294
+ meta = {
295
+ "model": model, "loras": loras, "controlnets": ctrl_selected,
296
+ "prompt": prompt, "negative": negative, "size": f"{width}x{height}",
297
+ "steps": steps, "cfg": cfg, "scheduler": scheduler_name, "seed": seed,
298
+ "post": {"upscale": do_upscale, "face_restore": do_face, "remove_bg": do_rembg}
299
+ }
300
+ return image, to_png_info(meta)
301
+
302
+ def run_img2img(model_id, custom_model, init_image, strength, **kw):
303
+ if init_image is None: raise gr.Error("โปรดอัปโหลดภาพเริ่มต้น (init image)")
304
+ model = (custom_model.strip() or model_id).strip()
305
+ pipe = StableDiffusionXLImg2ImgPipeline.from_pretrained(model, torch_dtype=dtype, use_safetensors=True)
306
+ pipe = pipe.to(device);
307
+ try:
308
+ if device=="cuda": pipe.enable_xformers_memory_efficient_attention()
309
+ except: pass
310
+ set_sched(pipe, kw["scheduler_name"]); gen = seed_gen(kw["seed"])
311
+ prompt = kw["prompt"] + (PRESETS.get(kw["preset"], "") if kw["preset"] else "")
312
+ negative = kw["negative"] or NEG_DEFAULT
313
+
314
+ if device=="cuda":
315
+ with torch.autocast("cuda"):
316
+ img = pipe(prompt=prompt, negative_prompt=negative, image=init_image,
317
+ strength=float(strength), num_inference_steps=int(kw["steps"]),
318
+ guidance_scale=float(kw["cfg"]), generator=gen).images[0]
319
+ else:
320
+ img = pipe(prompt=prompt, negative_prompt=negative, image=init_image,
321
+ strength=float(strength), num_inference_steps=int(kw["steps"]),
322
+ guidance_scale=float(kw["cfg"]), generator=gen).images[0]
323
+
324
+ img = post_process(img, kw["do_upscale"], kw["do_face"], kw["do_rembg"])
325
+ meta = {"mode":"img2img","model":model,"prompt":prompt,"neg":negative,"steps":kw["steps"],"cfg":kw["cfg"],"seed":kw["seed"],"strength":strength}
326
+ return img, to_png_info(meta)
327
+
328
+ def expand_canvas_for_outpaint(img: Image.Image, expand_px: int, direction: str) -> Tuple[Image.Image, Image.Image]:
329
+ w, h = img.size
330
+ if direction == "left": new = Image.new("RGBA", (w+expand_px, h), (0,0,0,0)); new.paste(img, (expand_px,0)); mask = Image.new("L", (w+expand_px,h), 0); ImageDraw = ImageDraw if 'ImageDraw' in globals() else __import__('PIL.ImageDraw').ImageDraw; d=ImageDraw.Draw(mask); d.rectangle([0,0,expand_px,h], fill=255)
331
+ elif direction == "right": new = Image.new("RGBA",(w+expand_px,h),(0,0,0,0)); new.paste(img,(0,0)); mask=Image.new("L",(w+expand_px,h),0); ImageDraw=ImageDraw if 'ImageDraw' in globals() else __import__('PIL.ImageDraw').ImageDraw; d=ImageDraw.Draw(mask); d.rectangle([w,0,w+expand_px,h], fill=255)
332
+ elif direction == "top": new = Image.new("RGBA",(w,h+expand_px),(0,0,0,0)); new.paste(img,(0,expand_px)); mask=Image.new("L",(w,h+expand_px),0); ImageDraw=ImageDraw if 'ImageDraw' in globals() else __import__('PIL.ImageDraw').ImageDraw; d=ImageDraw.Draw(mask); d.rectangle([0,0,w,expand_px], fill=255)
333
+ else: new = Image.new("RGBA",(w,h+expand_px),(0,0,0,0)); new.paste(img,(0,0)); mask=Image.new("L",(w,h+expand_px),0); ImageDraw=ImageDraw if 'ImageDraw' in globals() else __import__('PIL.ImageDraw').ImageDraw; d=ImageDraw.Draw(mask); d.rectangle([0,h,w,h+expand_px], fill=255)
334
+ return new.convert("RGB"), mask
335
+
336
+ def run_inpaint_outpaint(model_id, custom_model, base_image, mask_image, mode, expand_px, expand_dir, **kw):
337
+ if base_image is None: raise gr.Error("โปรดอัปโหลดภาพฐาน")
338
+ model = (custom_model.strip() or model_id).strip()
339
+ pipe = StableDiffusionXLInpaintPipeline.from_pretrained(model, torch_dtype=dtype, use_safetensors=True)
340
+ pipe = pipe.to(device);
341
+ try:
342
+ if device=="cuda": pipe.enable_xformers_memory_efficient_attention()
343
+ except: pass
344
+ set_sched(pipe, kw["scheduler_name"]); gen = seed_gen(kw["seed"])
345
+ prompt = kw["prompt"] + (PRESETS.get(kw["preset"], "") if kw["preset"] else "")
346
+ negative = kw["negative"] or NEG_DEFAULT
347
+
348
+ if mode == "Outpaint":
349
+ base_image, mask_image = expand_canvas_for_outpaint(base_image, int(expand_px), expand_dir)
350
+
351
+ if device=="cuda":
352
+ with torch.autocast("cuda"):
353
+ img = pipe(prompt=prompt, negative_prompt=negative,
354
+ image=base_image, mask_image=mask_image,
355
+ strength=kw.get("strength", 0.7),
356
+ num_inference_steps=int(kw["steps"]),
357
+ guidance_scale=float(kw["cfg"]),
358
+ generator=gen).images[0]
359
+ else:
360
+ img = pipe(prompt=prompt, negative_prompt=negative,
361
+ image=base_image, mask_image=mask_image,
362
+ strength=kw.get("strength", 0.7),
363
+ num_inference_steps=int(kw["steps"]),
364
+ guidance_scale=float(kw["cfg"]),
365
+ generator=gen).images[0]
366
+
367
+ img = post_process(img, kw["do_upscale"], kw["do_face"], kw["do_rembg"])
368
+ meta = {"mode":mode,"model":model,"prompt":prompt,"steps":kw["steps"],"cfg":kw["cfg"],"seed":kw["seed"]}
369
+ return img, to_png_info(meta)
370
+
371
+ # ---------------- UI ----------------
372
+ def build_ui():
373
+ with gr.Blocks(theme=gr.themes.Soft(), title="Masterpiece SDXL Studio Pro") as demo:
374
+ gr.Markdown("# 🖼️ Masterpiece SDXL Studio Pro")
375
+ gr.Markdown("เลือก **Models/LoRA/ControlNet** ได้หลายรายการ + **Img2Img / Inpaint / Outpaint** + **Upscale/FaceRestore/RemoveBG**")
376
+
377
+ # Common widgets
378
+ model_dd = gr.Dropdown(choices=[m[0] for m in MODELS], value=MODELS[0][0], label="Model (เลือก)")
379
+ model_custom = gr.Textbox(label="Custom Model ID (เช่น username/my-model)", placeholder="(ไม่จำเป็น)")
380
+
381
+ preset = gr.Dropdown(choices=list(PRESETS.keys()), value=None, label="Style Preset (optional)")
382
+ negative = gr.Textbox(value=NEG_DEFAULT, label="Negative Prompt")
383
+ steps = gr.Slider(10, 60, 30, step=1, label="Steps")
384
+ cfg = gr.Slider(1.0, 12.0, 7.0, step=0.1, label="CFG")
385
+ width = gr.Slider(512, 1024, 832, step=64, label="Width")
386
+ height= gr.Slider(512, 1024, 832, step=64, label="Height")
387
+ scheduler = gr.Dropdown(list(SCHEDULERS.keys()), value="DPM-Solver (Karras)", label="Scheduler")
388
+ seed = gr.Number(value=-1, precision=0, label="Seed (-1=random)")
389
+
390
+ # LoRA
391
+ lora_group = gr.CheckboxGroup(choices=[f"{rid} — {lbl} ({note})" for rid,lbl,note in LORAS], label="LoRA (เลือกหลายตัวได้)")
392
+ lora_custom = gr.Textbox(label="Custom LoRA IDs (คั่นด้วย comma)")
393
+ lora_s1 = gr.Slider(0.0, 1.2, 0.7, 0.05, label="LoRA scale #1")
394
+ lora_s2 = gr.Slider(0.0, 1.2, 0.5, 0.05, label="LoRA scale #2")
395
+ lora_s3 = gr.Slider(0.0, 1.2, 0.5, 0.05, label="LoRA scale #3")
396
+
397
+ # ControlNet
398
+ ctrl_group = gr.CheckboxGroup(choices=[c[1]+" ("+c[2]+")" for c in CONTROLNETS], label="ControlNet (เลือกชนิด)")
399
+ imgs = {
400
+ "canny": gr.Image(type="pil", label="Canny"),
401
+ "pose": gr.Image(type="pil", label="OpenPose"),
402
+ "depth": gr.Image(type="pil", label="Depth"),
403
+ "softedge": gr.Image(type="pil", label="SoftEdge"),
404
+ "lineart": gr.Image(type="pil", label="Lineart"),
405
+ "anime_lineart": gr.Image(type="pil", label="Anime Lineart"),
406
+ "normal": gr.Image(type="pil", label="Normal"),
407
+ "mlsd": gr.Image(type="pil", label="MLSD"),
408
+ "scribble": gr.Image(type="pil", label="Scribble"),
409
+ "seg": gr.Image(type="pil", label="Segmentation"),
410
+ "tile": gr.Image(type="pil", label="Tile"),
411
+ }
412
+
413
+ # Post-process
414
+ with gr.Row():
415
+ do_upscale = gr.Checkbox(False, label="Upscale x4 (ถ้ามี)")
416
+ do_face = gr.Checkbox(False, label="Face Restore (ถ้ามี)")
417
+ do_rembg = gr.Checkbox(False, label="Remove Background (ถ้ามี)")
418
+
419
+ with gr.Tab("Text → Image"):
420
+ prompt_txt = gr.Textbox(lines=3, label="Prompt")
421
+ btn_txt = gr.Button("🚀 Generate")
422
+ out_img_txt = gr.Image(type="pil", label="Result")
423
+ out_meta_txt = gr.Textbox(label="Metadata", lines=10)
424
+
425
+ with gr.Tab("Image → Image"):
426
+ init_img = gr.Image(type="pil", label="Init Image (img2img)")
427
+ strength = gr.Slider(0.1, 1.0, 0.7, 0.05, label="Strength")
428
+ prompt_i2i = gr.Textbox(lines=3, label="Prompt")
429
+ btn_i2i = gr.Button("🚀 Img2Img")
430
+ out_img_i2i = gr.Image(type="pil", label="Result")
431
+ out_meta_i2i = gr.Textbox(label="Metadata", lines=10)
432
+
433
+ with gr.Tab("Inpaint / Outpaint"):
434
+ base_img = gr.Image(type="pil", label="Base Image")
435
+ mask_img = gr.Image(type="pil", label="Mask (ขาว=แก้, ดำ=คงเดิม)")
436
+ mode_io = gr.Radio(["Inpaint","Outpaint"], value="Inpaint", label="Mode")
437
+ expand_px = gr.Slider(64, 1024, 256, 64, label="Outpaint pixels")
438
+ expand_dir = gr.Radio(["left","right","top","bottom"], value="right", label="Outpaint direction")
439
+ prompt_io = gr.Textbox(lines=3, label="Prompt")
440
+ btn_io = gr.Button("🚀 Inpaint/Outpaint")
441
+ out_img_io = gr.Image(type="pil", label="Result")
442
+ out_meta_io = gr.Textbox(label="Metadata", lines=10)
443
+
444
+ def parse_lora_list(selected: List[str]) -> List[str]:
445
+ if not selected: return []
446
+ out = []
447
+ for s in selected:
448
+ rid = s.split(" — ")[0].strip()
449
+ out.append(rid)
450
+ return out
451
+
452
+ btn_txt.click(
453
+ fn=run_txt2img,
454
+ inputs=[
455
+ model_dd, model_custom, prompt_txt, preset, negative,
456
+ steps, cfg, width, height, scheduler, seed,
457
+ gr.Variable(parse_lora_list), lora_custom, lora_s1, lora_s2, lora_s3,
458
+ ctrl_group,
459
+ {k:v for k,v in imgs.items()}, # dict of images
460
+ gr.Checkbox(False), gr.Slider(0.05,0.5,0.2,0.05),
461
+ do_upscale, do_face, do_rembg
462
+ ],
463
+ outputs=[out_img_txt, out_meta_txt],
464
+ api_name="txt2img"
465
+ )
466
+
467
+ btn_i2i.click(
468
+ fn=run_img2img,
469
+ inputs=[model_dd, model_custom, init_img, strength,
470
+ ("prompt",), preset, negative, steps, cfg, width, height, scheduler, seed,
471
+ do_upscale, do_face, do_rembg],
472
+ outputs=[out_img_i2i, out_meta_i2i],
473
+ api_name="img2img"
474
+ )
475
+
476
+ btn_io.click(
477
+ fn=run_inpaint_outpaint,
478
+ inputs=[model_dd, model_custom, base_img, mask_img, mode_io, expand_px, expand_dir,
479
+ ("prompt",), preset, negative, steps, cfg, width, height, scheduler, seed,
480
+ strength, do_upscale, do_face, do_rembg],
481
+ outputs=[out_img_io, out_meta_io],
482
+ api_name="inpaint_outpaint"
483
+ )
484
+
485
+ gr.Markdown("ℹ️ **หมายเหตุ**: ถ้า LoRA/ControlNet/โพสต์โปรเซสบางตัวไม่มีในสภาพแวดล้อม โปรแกรมจะข้ามให้อัตโนมัติและแจ้งใน Console")
486
+
487
+ return demo
488
+
489
+ demo = build_ui()
490
+ demo.queue(max_size=8).launch()