import os, io, time, json, math from typing import List, Dict, Optional, Tuple import gradio as gr import numpy as np from PIL import Image, ImageOps import torch from diffusers import ( StableDiffusionXLPipeline, StableDiffusionXLImg2ImgPipeline, StableDiffusionXLInpaintPipeline, StableDiffusionXLControlNetPipeline, ControlNetModel, StableDiffusionUpscalePipeline, DPMSolverMultistepScheduler, EulerDiscreteScheduler, EulerAncestralDiscreteScheduler, HeunDiscreteScheduler, ) # ---------- Optional deps ---------- try: from rembg import remove as rembg_remove except Exception: rembg_remove = None # face restore try: from gfpgan import GFPGANer _HAS_GFP = True except Exception: _HAS_GFP = False # realesrgan (fallback upscaler) try: from realesrgan import RealESRGAN _HAS_REALESRGAN = True except Exception: _HAS_REALESRGAN = False device = "cuda" if torch.cuda.is_available() else "cpu" dtype = torch.float16 if device == "cuda" else torch.float32 # ---------------- Registry (เปลี่ยน/เพิ่มได้เอง) ---------------- MODELS: List[Tuple[str,str,str]] = [ # (id, label, note) ("stabilityai/stable-diffusion-xl-base-1.0", "SDXL Base 1.0", "เอนกประสงค์ สมดุล"), ("stabilityai/stable-diffusion-xl-refiner-1.0","SDXL Refiner", "เก็บรายละเอียด (pass 2)"), ("SG161222/RealVisXL_V4.0", "RealVis XL v4", "โฟโต้เรียล คน/สินค้าเนียน"), ("Lykon/dreamshaper-xl-v2", "DreamShaper XL","แฟนตาซี–เรียลลิสติกหลากสไตล์"), ("RunDiffusion/Juggernaut-XL", "Juggernaut XL", "คอนทราสต์แรง รายละเอียดหนัก"), ("emilianJR/epiCRealismXL", "EpicRealism XL","แฟชั่น/พอร์เทรตคอนทราสต์ดี"), ("black-forest-labs/FLUX.1-dev", "FLUX.1-dev", "สมัยใหม่/คุมสไตล์ดี (ไม่ใช่ SDXL)"), ("stabilityai/sd-turbo", "SD-Turbo", "ไวมาก เหมาะกับร่างไอเดีย"), ("stabilityai/stable-diffusion-2-1", "SD 2.1", "แลนด์สเคป/องค์ประกอบกว้าง"), ("runwayml/stable-diffusion-v1-5", "SD 1.5", "คลาสสิก/ทรัพยากรเยอะ"), ("timbrooks/instruct-pix2pix", "Instruct-Pix2Pix","แก้ภาพตามคำสั่ง (Img2Img)"), ] LORAS: List[Tuple[str,str,str]] = [ # หมายเหตุ: รายชื่อ LoRA แพร่หลายและเปลี่ยนเร็ว; ถ้าโหลดไม่ได้ โปรแกรมจะไม่ล้ม ("ByteDance/SDXL-Lightning", "SDXL-Lightning", "สปีดเร็ว (LoRA)"), ("ostris/epicrealism-xl-lora", "EpicrealismXL-LoRA","โทนเรียลลิสติก"), ("XLabs-AI/flux-prompt-lora", "FLUX Prompt LoRA", "ปรับ prompt style (FLUX)"), ("XLabs-AI/realvisxl-v4-lora", "RealVisXL LoRA", "พอร์เทรต/สินค้า"), ("alpha-diffusion/sdxl-anime-lora", "Anime-Style XL", "อนิเม/เส้นใส"), ("alpha-diffusion/sdxl-cinematic-lora", "Cinematic-Drama", "แสงเงาแบบหนัง"), ("alpha-diffusion/sdxl-watercolor-lora", "Watercolor-Soft", "สีน้ำ/พาสเทล"), ("alpha-diffusion/sdxl-fashion-lora", "Fashion-Editorial","แฟชั่น/กองถ่าย"), ("alpha-diffusion/sdxl-product-lora", "Product-Studio", "สินค้า/แสงสตูดิโอ"), ("alpha-diffusion/sdxl-interior-lora", "Interior-Archi", "ห้อง/สถาปัตย์"), ("alpha-diffusion/sdxl-food-lora", "Food-Tasty", "อาหารฉ่ำ/เงางาม"), ] CONTROLNETS: List[Tuple[str,str,str,str]] = [ # (id, label, note, key) ("diffusers/controlnet-canny-sdxl-1.0", "Canny", "คุมเส้นขอบ", "canny"), ("diffusers/controlnet-openpose-sdxl-1.0", "OpenPose", "คุมท่าทางคน", "pose"), ("diffusers/controlnet-depth-sdxl-1.0", "Depth", "คุมมุมมอง/ระยะลึก", "depth"), ("diffusers/controlnet-softedge-sdxl-1.0", "SoftEdge", "เส้นนุ่ม/ลดแตก", "softedge"), ("diffusers/controlnet-lineart-sdxl-1.0", "Lineart", "เส้นร่าง/การ์ตูน", "lineart"), ("diffusers/controlnet-anime-lineart-sdxl-1.0","Anime Lineart","เส้นอนิเม", "anime_lineart"), ("diffusers/controlnet-normal-sdxl-1.0", "Normal", "ทิศทางพื้นผิว", "normal"), ("diffusers/controlnet-mlsd-sdxl-1.0", "MLSD", "เส้นตรง/สถาปัตย์", "mlsd"), ("diffusers/controlnet-scribble-sdxl-1.0", "Scribble", "สเก็ตช์หยาบ→จริง", "scribble"), ("diffusers/controlnet-seg-sdxl-1.0", "Segmentation", "แบ่งส่วน/สี", "seg"), ("diffusers/controlnet-tile-sdxl-1.0", "Tile", "อัปสเกลแบบกระเบื้อง", "tile"), ] PRESETS = { "Cinematic": ", cinematic lighting, 50mm, bokeh, film grain, high dynamic range", "Studio": ", studio photo, softbox lighting, sharp focus, high detail", "Product": ", product photography, seamless background, diffused light, reflections", "Anime": ", anime style, clean lineart, vibrant colors, high quality", } NEG_DEFAULT = "lowres, blurry, bad anatomy, extra fingers, watermark, jpeg artifacts, text" SCHEDULERS = { "DPM-Solver (Karras)": DPMSolverMultistepScheduler, "Euler": EulerDiscreteScheduler, "Euler a": EulerAncestralDiscreteScheduler, "Heun": HeunDiscreteScheduler, } # ---------------- Cache & helpers ---------------- PIPE_CACHE: Dict[str, object] = {} CONTROL_CACHE: Dict[str, ControlNetModel] = {} UPSCALE_PIPE: Optional[StableDiffusionUpscalePipeline] = None GFP: Optional[GFPGANer] = None REALSR: Optional[RealESRGAN] = None def set_sched(pipe, name: str): cls = SCHEDULERS.get(name, DPMSolverMultistepScheduler) pipe.scheduler = cls.from_config(pipe.scheduler.config) def seed_gen(sd: int): if sd is None or sd < 0: return None g = torch.Generator(device=device if device=="cuda" else "cpu") g.manual_seed(int(sd)) return g def prep_pipe(model_id: str, control_ids: List[str]): key = f"{model_id}|{'-'.join(control_ids) if control_ids else 'none'}" if key in PIPE_CACHE: return PIPE_CACHE[key] if control_ids: cns = [] for cid in control_ids: if cid not in CONTROL_CACHE: CONTROL_CACHE[cid] = ControlNetModel.from_pretrained(cid, torch_dtype=dtype, use_safetensors=True) cns.append(CONTROL_CACHE[cid]) pipe = StableDiffusionXLControlNetPipeline.from_pretrained(model_id, controlnet=cns, torch_dtype=dtype, use_safetensors=True) else: pipe = StableDiffusionXLPipeline.from_pretrained(model_id, torch_dtype=dtype, use_safetensors=True) if device == "cuda": pipe.to("cuda") pipe.enable_vae_tiling(); pipe.enable_vae_slicing() try: pipe.enable_xformers_memory_efficient_attention() except Exception: pass else: pipe.to("cpu"); pipe.enable_attention_slicing() PIPE_CACHE[key] = pipe return pipe def apply_loras(pipe, ids: List[str], scales: List[float]): for i, rid in enumerate([x for x in ids if x]): try: pipe.load_lora_weights(rid) try: sc = scales[i] if i < len(scales) else 0.7 pipe.fuse_lora(lora_scale=float(sc)) except Exception: pass except Exception as e: print(f"[LoRA] load failed {rid}: {e}") def to_png_info(meta: dict) -> str: return json.dumps(meta, ensure_ascii=False, indent=2) # ---------------- Optional Post-process ---------------- def ensure_upscalers(): global UPSCALE_PIPE, GFP, REALSR if UPSCALE_PIPE is None: try: UPSCALE_PIPE = StableDiffusionUpscalePipeline.from_pretrained( "stabilityai/stable-diffusion-x4-upscaler", torch_dtype=torch.float16 if device=="cuda" else torch.float32, use_safetensors=True ).to(device) except Exception as e: print("[Upscaler] SD x4 not available:", e) if _HAS_GFP and GFP is None: try: GFP = GFPGANer(model_path=None, upscale=1, arch="clean", channel_multiplier=2) except Exception as e: print("[GFPGAN] init failed:", e) if _HAS_REALESRGAN and REALSR is None and device == "cuda": try: REALSR = RealESRGAN(torch.device("cuda"), scale=4) REALSR.load_weights("weights/RealESRGAN_x4plus.pth") # ใช้ได้เมื่อมีไฟล์ (ถ้าไม่มีจะข้าม) except Exception as e: print("[RealESRGAN] init failed:", e) def post_process(img: Image.Image, do_upscale: bool, do_face: bool, do_rembg: bool): ensure_upscalers() out = img # Upscale priority: RealESRGAN > SD x4 > none if do_upscale: try: if REALSR is not None: out = Image.fromarray(REALSR.predict(np.array(out))) elif UPSCALE_PIPE is not None: if device == "cuda": with torch.autocast("cuda"): out = UPSCALE_PIPE(prompt="", image=out).images[0] else: out = UPSCALE_PIPE(prompt="", image=out).images[0] except Exception as e: print("[Upscale] skipped:", e) if do_face and _HAS_GFP and GFP is not None: try: _, _, out = GFP.enhance(np.array(out), has_aligned=False, only_center_face=False, paste_back=True) out = Image.fromarray(out) except Exception as e: print("[GFPGAN] skipped:", e) if do_rembg and rembg_remove is not None: try: out = Image.open(io.BytesIO(rembg_remove(np.array(out)))) except Exception as e: print("[rembg] skipped:", e) return out # ---------------- Core generate ---------------- def run_txt2img(model_id, custom_model, prompt, preset, negative, steps, cfg, width, height, scheduler_name, seed, lora_list, lora_custom_csv, lora_s1, lora_s2, lora_s3, ctrl_selected, ctrl_images, use_refiner, refine_strength, do_upscale, do_face, do_rembg): if not prompt.strip(): raise gr.Error("กรุณากรอก prompt") model = (custom_model.strip() or model_id).strip() if preset in PRESETS: prompt = prompt + PRESETS[preset] if not negative.strip(): negative = NEG_DEFAULT # Collect control images cond_imgs, ctrl_ids = [], [] for (cid, label, note, key) in CONTROLNETS: if label in ctrl_selected and key in ctrl_images and ctrl_images[key] is not None: ctrl_ids.append(cid); cond_imgs.append(ctrl_images[key]) pipe = prep_pipe(model, ctrl_ids) set_sched(pipe, scheduler_name) loras = [] if lora_list: loras += lora_list if lora_custom_csv.strip(): loras += [x.strip() for x in lora_custom_csv.split(",") if x.strip()] apply_loras(pipe, loras, [lora_s1, lora_s2, lora_s3]) width = int(max(512, min(1024, width))) height = int(max(512, min(1024, height))) gen = seed_gen(seed) if device == "cuda": with torch.autocast("cuda"): if ctrl_ids: image = pipe(prompt=prompt, negative_prompt=negative, width=width, height=height, num_inference_steps=int(steps), guidance_scale=float(cfg), controlnet_conditioning_image=cond_imgs if len(cond_imgs)>1 else cond_imgs[0], generator=gen).images[0] else: image = pipe(prompt=prompt, negative_prompt=negative, width=width, height=height, num_inference_steps=int(steps), guidance_scale=float(cfg), generator=gen).images[0] else: if ctrl_ids: image = pipe(prompt=prompt, negative_prompt=negative, width=width, height=height, num_inference_steps=int(steps), guidance_scale=float(cfg), controlnet_conditioning_image=cond_imgs if len(cond_imgs)>1 else cond_imgs[0], generator=gen).images[0] else: image = pipe(prompt=prompt, negative_prompt=negative, width=width, height=height, num_inference_steps=int(steps), guidance_scale=float(cfg), generator=gen).images[0] # Refiner (GPU) if use_refiner and device == "cuda": try: ref = StableDiffusionXLImg2ImgPipeline.from_pretrained( "stabilityai/stable-diffusion-xl-refiner-1.0", torch_dtype=torch.float16, use_safetensors=True ).to("cuda") set_sched(ref, scheduler_name) with torch.autocast("cuda"): image = ref(prompt=prompt, negative_prompt=negative, image=image, strength=float(refine_strength), num_inference_steps=max(10, int(steps)//2), guidance_scale=float(cfg), generator=gen).images[0] except Exception as e: print("[Refiner] skipped:", e) # Post-process image = post_process(image, do_upscale, do_face, do_rembg) meta = { "model": model, "loras": loras, "controlnets": ctrl_selected, "prompt": prompt, "negative": negative, "size": f"{width}x{height}", "steps": steps, "cfg": cfg, "scheduler": scheduler_name, "seed": seed, "post": {"upscale": do_upscale, "face_restore": do_face, "remove_bg": do_rembg} } return image, to_png_info(meta) def run_img2img(model_id, custom_model, init_image, strength, **kw): if init_image is None: raise gr.Error("โปรดอัปโหลดภาพเริ่มต้น (init image)") model = (custom_model.strip() or model_id).strip() pipe = StableDiffusionXLImg2ImgPipeline.from_pretrained(model, torch_dtype=dtype, use_safetensors=True) pipe = pipe.to(device); try: if device=="cuda": pipe.enable_xformers_memory_efficient_attention() except: pass set_sched(pipe, kw["scheduler_name"]); gen = seed_gen(kw["seed"]) prompt = kw["prompt"] + (PRESETS.get(kw["preset"], "") if kw["preset"] else "") negative = kw["negative"] or NEG_DEFAULT if device=="cuda": with torch.autocast("cuda"): img = pipe(prompt=prompt, negative_prompt=negative, image=init_image, strength=float(strength), num_inference_steps=int(kw["steps"]), guidance_scale=float(kw["cfg"]), generator=gen).images[0] else: img = pipe(prompt=prompt, negative_prompt=negative, image=init_image, strength=float(strength), num_inference_steps=int(kw["steps"]), guidance_scale=float(kw["cfg"]), generator=gen).images[0] img = post_process(img, kw["do_upscale"], kw["do_face"], kw["do_rembg"]) meta = {"mode":"img2img","model":model,"prompt":prompt,"neg":negative,"steps":kw["steps"],"cfg":kw["cfg"],"seed":kw["seed"],"strength":strength} return img, to_png_info(meta) def expand_canvas_for_outpaint(img: Image.Image, expand_px: int, direction: str) -> Tuple[Image.Image, Image.Image]: w, h = img.size 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) 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) 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) 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) return new.convert("RGB"), mask def run_inpaint_outpaint(model_id, custom_model, base_image, mask_image, mode, expand_px, expand_dir, **kw): if base_image is None: raise gr.Error("โปรดอัปโหลดภาพฐาน") model = (custom_model.strip() or model_id).strip() pipe = StableDiffusionXLInpaintPipeline.from_pretrained(model, torch_dtype=dtype, use_safetensors=True) pipe = pipe.to(device); try: if device=="cuda": pipe.enable_xformers_memory_efficient_attention() except: pass set_sched(pipe, kw["scheduler_name"]); gen = seed_gen(kw["seed"]) prompt = kw["prompt"] + (PRESETS.get(kw["preset"], "") if kw["preset"] else "") negative = kw["negative"] or NEG_DEFAULT if mode == "Outpaint": base_image, mask_image = expand_canvas_for_outpaint(base_image, int(expand_px), expand_dir) if device=="cuda": with torch.autocast("cuda"): img = pipe(prompt=prompt, negative_prompt=negative, image=base_image, mask_image=mask_image, strength=kw.get("strength", 0.7), num_inference_steps=int(kw["steps"]), guidance_scale=float(kw["cfg"]), generator=gen).images[0] else: img = pipe(prompt=prompt, negative_prompt=negative, image=base_image, mask_image=mask_image, strength=kw.get("strength", 0.7), num_inference_steps=int(kw["steps"]), guidance_scale=float(kw["cfg"]), generator=gen).images[0] img = post_process(img, kw["do_upscale"], kw["do_face"], kw["do_rembg"]) meta = {"mode":mode,"model":model,"prompt":prompt,"steps":kw["steps"],"cfg":kw["cfg"],"seed":kw["seed"]} return img, to_png_info(meta) # ---------------- UI ---------------- def build_ui(): with gr.Blocks(theme=gr.themes.Soft(), title="Masterpiece SDXL Studio Pro") as demo: gr.Markdown("# 🖼️ Masterpiece SDXL Studio Pro") gr.Markdown("เลือก **Models/LoRA/ControlNet** ได้หลายรายการ + **Img2Img / Inpaint / Outpaint** + **Upscale/FaceRestore/RemoveBG**") # Common widgets model_dd = gr.Dropdown(choices=[m[0] for m in MODELS], value=MODELS[0][0], label="Model (เลือก)") model_custom = gr.Textbox(label="Custom Model ID (เช่น username/my-model)", placeholder="(ไม่จำเป็น)") preset = gr.Dropdown(choices=list(PRESETS.keys()), value=None, label="Style Preset (optional)") negative = gr.Textbox(value=NEG_DEFAULT, label="Negative Prompt") steps = gr.Slider(10, 60, 30, step=1, label="Steps") cfg = gr.Slider(1.0, 12.0, 7.0, step=0.1, label="CFG") width = gr.Slider(512, 1024, 832, step=64, label="Width") height= gr.Slider(512, 1024, 832, step=64, label="Height") scheduler = gr.Dropdown(list(SCHEDULERS.keys()), value="DPM-Solver (Karras)", label="Scheduler") seed = gr.Number(value=-1, precision=0, label="Seed (-1=random)") # LoRA lora_group = gr.CheckboxGroup(choices=[f"{rid} — {lbl} ({note})" for rid,lbl,note in LORAS], label="LoRA (เลือกหลายตัวได้)") lora_custom = gr.Textbox(label="Custom LoRA IDs (คั่นด้วย comma)") lora_s1 = gr.Slider(0.0, 1.2, 0.7, 0.05, label="LoRA scale #1") lora_s2 = gr.Slider(0.0, 1.2, 0.5, 0.05, label="LoRA scale #2") lora_s3 = gr.Slider(0.0, 1.2, 0.5, 0.05, label="LoRA scale #3") # ControlNet ctrl_group = gr.CheckboxGroup(choices=[c[1]+" ("+c[2]+")" for c in CONTROLNETS], label="ControlNet (เลือกชนิด)") imgs = { "canny": gr.Image(type="pil", label="Canny"), "pose": gr.Image(type="pil", label="OpenPose"), "depth": gr.Image(type="pil", label="Depth"), "softedge": gr.Image(type="pil", label="SoftEdge"), "lineart": gr.Image(type="pil", label="Lineart"), "anime_lineart": gr.Image(type="pil", label="Anime Lineart"), "normal": gr.Image(type="pil", label="Normal"), "mlsd": gr.Image(type="pil", label="MLSD"), "scribble": gr.Image(type="pil", label="Scribble"), "seg": gr.Image(type="pil", label="Segmentation"), "tile": gr.Image(type="pil", label="Tile"), } # Post-process with gr.Row(): do_upscale = gr.Checkbox(False, label="Upscale x4 (ถ้ามี)") do_face = gr.Checkbox(False, label="Face Restore (ถ้ามี)") do_rembg = gr.Checkbox(False, label="Remove Background (ถ้ามี)") with gr.Tab("Text → Image"): prompt_txt = gr.Textbox(lines=3, label="Prompt") btn_txt = gr.Button("🚀 Generate") out_img_txt = gr.Image(type="pil", label="Result") out_meta_txt = gr.Textbox(label="Metadata", lines=10) with gr.Tab("Image → Image"): init_img = gr.Image(type="pil", label="Init Image (img2img)") strength = gr.Slider(0.1, 1.0, 0.7, 0.05, label="Strength") prompt_i2i = gr.Textbox(lines=3, label="Prompt") btn_i2i = gr.Button("🚀 Img2Img") out_img_i2i = gr.Image(type="pil", label="Result") out_meta_i2i = gr.Textbox(label="Metadata", lines=10) with gr.Tab("Inpaint / Outpaint"): base_img = gr.Image(type="pil", label="Base Image") mask_img = gr.Image(type="pil", label="Mask (ขาว=แก้, ดำ=คงเดิม)") mode_io = gr.Radio(["Inpaint","Outpaint"], value="Inpaint", label="Mode") expand_px = gr.Slider(64, 1024, 256, 64, label="Outpaint pixels") expand_dir = gr.Radio(["left","right","top","bottom"], value="right", label="Outpaint direction") prompt_io = gr.Textbox(lines=3, label="Prompt") btn_io = gr.Button("🚀 Inpaint/Outpaint") out_img_io = gr.Image(type="pil", label="Result") out_meta_io = gr.Textbox(label="Metadata", lines=10) def parse_lora_list(selected: List[str]) -> List[str]: if not selected: return [] out = [] for s in selected: rid = s.split(" — ")[0].strip() out.append(rid) return out btn_txt.click( fn=run_txt2img, inputs=[ model_dd, model_custom, prompt_txt, preset, negative, steps, cfg, width, height, scheduler, seed, gr.Variable(parse_lora_list), lora_custom, lora_s1, lora_s2, lora_s3, ctrl_group, {k:v for k,v in imgs.items()}, # dict of images gr.Checkbox(False), gr.Slider(0.05,0.5,0.2,0.05), do_upscale, do_face, do_rembg ], outputs=[out_img_txt, out_meta_txt], api_name="txt2img" ) btn_i2i.click( fn=run_img2img, inputs=[model_dd, model_custom, init_img, strength, ("prompt",), preset, negative, steps, cfg, width, height, scheduler, seed, do_upscale, do_face, do_rembg], outputs=[out_img_i2i, out_meta_i2i], api_name="img2img" ) btn_io.click( fn=run_inpaint_outpaint, inputs=[model_dd, model_custom, base_img, mask_img, mode_io, expand_px, expand_dir, ("prompt",), preset, negative, steps, cfg, width, height, scheduler, seed, strength, do_upscale, do_face, do_rembg], outputs=[out_img_io, out_meta_io], api_name="inpaint_outpaint" ) gr.Markdown("ℹ️ **หมายเหตุ**: ถ้า LoRA/ControlNet/โพสต์โปรเซสบางตัวไม่มีในสภาพแวดล้อม โปรแกรมจะข้ามให้อัตโนมัติและแจ้งใน Console") return demo demo = build_ui() demo.queue(max_size=8).launch()