""" Character Studio — a ZeroGPU Hugging Face Space. A multi-model character generator: pick a model from an editable registry, type a prompt, optionally drop a reference image, and generate. Supports SD1.5 / SDXL / FLUX bases and txt2img / img2img / IP-Adapter / FaceID modes. Add or remove models by editing models.json (no code change needed), then click "🔄 Reload models" or restart the Space. """ import random import traceback import spaces # must be imported before torch on ZeroGPU import gradio as gr # --- Workaround for a gradio_client bug: "argument of type 'bool' is not iterable". # Some component api_info schemas carry a boolean `additionalProperties`, which the # schema-to-type walker chokes on, crashing get_api_info() and the whole launch. # Short-circuit any non-dict schema to "Any". Version-independent and harmless. --- import gradio_client.utils as _gcu _orig_get_type = _gcu.get_type def _safe_get_type(schema): if not isinstance(schema, dict): return "Any" return _orig_get_type(schema) _gcu.get_type = _safe_get_type _orig_j2pt = _gcu._json_schema_to_python_type def _safe_j2pt(schema, *args, **kwargs): if isinstance(schema, bool): return "Any" return _orig_j2pt(schema, *args, **kwargs) _gcu._json_schema_to_python_type = _safe_j2pt import pipeline_manager as pm MAX_SEED = 2**31 - 1 # --------------------------------------------------------------------------- # Registry helpers # --------------------------------------------------------------------------- def load_models(): return pm.load_registry() MODELS = load_models() def model_choices(models): return [(m["label"], m["id"]) for m in models] # Placeholder shown in the picker when a model has no `preview` image. _FALLBACK_PREVIEW = "https://placehold.co/400x520/1e293b/93c5fd/png?text=Model" def gallery_items(models): """List of (image_url, caption) for the model picker gallery.""" return [(m.get("preview") or _FALLBACK_PREVIEW, m["label"]) for m in models] def model_ids(models): return [m["id"] for m in models] def modes_for(models, model_id): m = pm.get_model(models, model_id) if not m: return [("Text → Image", "txt2img")] return [(pm.MODE_LABELS[k], k) for k in pm.SUPPORTED_MODES[m["base"]]] # --------------------------------------------------------------------------- # GPU generation # --------------------------------------------------------------------------- @spaces.GPU(duration=120) def generate(model_id, mode, prompt, negative_prompt, ref_image, steps, guidance, denoise, ip_scale, width, height, seed, randomize, translator, pose_ref_image=None): models = load_models() cfg = pm.get_model(models, model_id) if cfg is None: raise gr.Error("ไม่พบโมเดลที่เลือก โปรด Reload models / Selected model not found.") # Prompt Builder pose reference: if the user uploaded a pose image, lock the pose # to it via ControlNet OpenPose (reuses the tested 'pose' path) regardless of the # mode radio — face/clothes/scene still come from the text prompt. if pose_ref_image is not None: if "pose" in pm.SUPPORTED_MODES.get(cfg["base"], []): mode = "pose" ref_image = pose_ref_image else: raise gr.Error("โมเดลนี้ไม่รองรับ Pose lock (ใช้ได้กับ SD1.5 / SDXL) — " "เลือกโมเดล SD1.5 เช่น majicMIX แล้วลองใหม่") if randomize or seed is None or int(seed) < 0: seed = random.randint(0, MAX_SEED) # Thai → English so the (English) text encoders understand the prompt. note = "" orig_prompt = prompt prompt = pm.translate_prompt(prompt, translator) negative_prompt = pm.translate_prompt(negative_prompt, translator) # --- Post-translation fixes (the translator often drops/softens these) --- # 1) Full-body framing: detect intent from the ORIGINAL Thai (translator tends to # drop "เต็มตัว/ทั้งตัว"); inject an explicit English tag up front so the model AND # the auto-tall-canvas logic in run_generation reliably honor it. if pm.wants_full_body(orig_prompt) or pm.wants_full_body(prompt): if "full body" not in prompt.lower(): prompt = "full body shot, head to toe, full body visible, " + prompt # 2) Gaze: SD1.5 obeys the booru tag "looking at viewer" far better than "...camera". if "looking at the camera" in prompt.lower() or "looking at camera" in prompt.lower(): prompt = (prompt.replace("looking at the camera", "looking at viewer, eye contact") .replace("looking at camera", "looking at viewer, eye contact")) elif "มองกล้อง" in (orig_prompt or "") and "looking at viewer" not in prompt.lower(): prompt = "looking at viewer, eye contact, " + prompt if prompt != orig_prompt: note = f" · 🌐 {translator}: _{prompt[:120]}_" try: img = pm.run_generation( cfg=cfg, mode=mode, prompt=prompt, negative_prompt=negative_prompt, ref_image=ref_image, steps=steps, guidance=guidance, denoise=denoise, ip_scale=ip_scale, width=width, height=height, seed=seed, ) except Exception as e: traceback.print_exc() raise gr.Error(str(e)) status = f"✅ {cfg['label']} · {pm.MODE_LABELS.get(mode, mode)} · seed {seed}{note}" return img, seed, status # --------------------------------------------------------------------------- # UI callbacks # --------------------------------------------------------------------------- def _apply_model(models, model_id): """Shared: given a selected model id, return the dependent UI updates.""" cfg = pm.get_model(models, model_id) if not cfg: return (*[gr.update() for _ in range(7)], "ไม่พบโมเดล / model not found") choices = modes_for(models, model_id) return ( gr.update(choices=choices, value=choices[0][1]), # mode radio gr.update(placeholder=cfg.get("recommended_prompt", "")), # prompt gr.update(value=cfg.get("negative_prompt", "")), # negative gr.update(value=cfg.get("default_steps", 28)), # steps gr.update(value=cfg.get("default_guidance", 6.0)), # guidance gr.update(value=cfg.get("default_width", 768)), # width gr.update(value=cfg.get("default_height", 768)), # height f"**เลือก / Selected:** {cfg['label']}", # selected label ) def on_gallery_select(ids, evt: gr.SelectData): """A model card was clicked. evt.index → model id.""" models = load_models() mid = ids[evt.index] if ids and 0 <= evt.index < len(ids) else None return (mid, *_apply_model(models, mid)) def reload_registry(): global MODELS MODELS = load_models() first = MODELS[0]["id"] if MODELS else None return ( gr.update(value=gallery_items(MODELS)), # gallery model_ids(MODELS), # ids state first, # selected id state f"🔄 โหลดแล้ว {len(MODELS)} โมเดล", ) def _field(v): """Normalise a Prompt Builder field to a clean string. Dropdowns with multiselect return a list — join those; plain Textboxes return a string.""" if isinstance(v, (list, tuple)): return ", ".join(str(x).strip() for x in v if x and str(x).strip()) return str(v).strip() if v is not None else "" def build_prompt(subject, age, ethnicity, skin, face, body, hair, eyes, outfit, pose, expression, scene, lighting, shot): """Assemble form fields into a prompt. Thai values are fine — the auto-translator turns the whole thing into English at generate time. Dropdown fields already carry model-friendly English tags (e.g. 'hourglass figure').""" parts = [] # Framing/shot goes FIRST so "full body / head to toe" survives CLIP's 77-token cut # (it's the key compositional cue; if truncated, SD1.5 defaults to a portrait crop). shot = _field(shot) if shot: parts.append(shot) who = _field(subject) or "ผู้หญิง" ethnicity, age = _field(ethnicity), _field(age) if ethnicity: who = f"{ethnicity} {who}" if age: who = f"{who} อายุ {age} ปี" parts.append(who) # Priority order for CLIP's 77-token budget: compositional anchors first # (location/lighting/outfit/pose), fine appearance details last (least harmful # if truncated). Skin texture realism is carried by the model's style_prefix anyway. for v in (scene, lighting, outfit, pose, expression, body, hair, skin, face, eyes): s = _field(v) if s: parts.append(s) thai = ", ".join(parts) # Photographic suffix (NOT "masterpiece/best quality" — those art tokens push the model # toward an illustrated/CG look. The model's style_prefix + negative do the heavy lifting). return thai + ", realistic candid photograph, natural skin texture, soft natural light" # --------------------------------------------------------------------------- # Layout (mirrors the FLUX LoRA DLC reference UI) # --------------------------------------------------------------------------- def build_face_prompt(gender, age, ethnicity, faceshape, skin, eyecolor, eyes, brow, nose, mouth, hair, expr): """Assemble Tab-1 face fields into an identity-focused prompt (Thai ok).""" who = _field(gender) or "ผู้หญิง" e, a = _field(ethnicity), _field(age) if e: who = f"{e} {who}" if a: who = f"{who} อายุ {a} ปี" parts = [who] for v in (faceshape, skin, eyecolor, eyes, brow, nose, mouth, hair, expr): s = _field(v) if s: parts.append(s) return ", ".join(parts) + ", detailed face, beautiful detailed eyes, detailed skin" @spaces.GPU(duration=120) def generate_face(model_id, prompt, translator): """Tab 1: generate a close-up identity portrait to lock the face first.""" models = load_models() cfg = pm.get_model(models, model_id) if cfg is None: raise gr.Error("ไม่พบโมเดล — กด Reload models ในแถบ 2 / model not found") if not (prompt and str(prompt).strip()): raise gr.Error("กรอกช่องใบหน้าแล้วกด '✨ สร้าง prompt ใบหน้า' ก่อน") import random as _r seed = _r.randint(0, MAX_SEED) p = pm.translate_prompt(prompt, translator) # Head-and-shoulders framing with headroom (NOT extreme close-up) + a strong skin- # realism push baked in here, because only majicMIX has a style_prefix — this makes # ALL models (incl. SDXL base) render real skin in the identity tab. Front-loaded so # it survives CLIP truncation. p = ("RAW photo, photograph, upper body portrait, head and shoulders, full head in " "frame, headroom above the head, centered, looking at viewer, " "(photorealistic skin:1.1), detailed skin texture, visible skin pores, " "fine vellus hair, film grain, " + p) # push away from macro/cropped framings + plastic/CG skin neg = (cfg.get("negative_prompt", "") + ", (extreme close-up, macro, cropped head, head out of frame, forehead cut off:1.3)" ", (plastic skin, airbrushed, smooth skin, waxy skin, doll, 3d, cgi, render:1.2)") # Gender lock: "short hair / square jaw / pointed nose" bias SD1.5 toward MALE and can # override a lone gender word. Default to female unless the user explicitly picked male, # and add strong male negatives so "woman" sticks. _raw = (prompt or "").lower() _is_male = ("1boy" in _raw) or ("ผู้ชาย" in (prompt or "")) or ("ชาย" in (prompt or "") and "หญิง" not in (prompt or "")) if not _is_male: neg += ", (man, male, masculine, beard, mustache, facial hair, adam's apple:1.4)" # SDXL models need their native ~1024 canvas; SD1.5 stays 512-wide. fw, fh = (768, 1024) if cfg["base"] == "sdxl" else (512, 768) try: img = pm.run_generation( cfg=cfg, mode="txt2img", prompt=p, negative_prompt=neg, ref_image=None, steps=int(cfg.get("default_steps", 30)), guidance=float(cfg.get("default_guidance", 5.0)), denoise=0.4, ip_scale=0.7, width=fw, height=fh, seed=seed, ) except Exception as e: # noqa traceback.print_exc() raise gr.Error(str(e)) return img, f"✅ ใบหน้า · {cfg['label']} · seed {seed}" def send_face_to_scene(face_img): """Copy the Tab-1 face into Tab-2's reference image + switch to FaceID.""" if face_img is None: return gr.update(), gr.update(), "⚠️ ยังไม่มีใบหน้า — กด 'สร้างใบหน้า' ก่อน" return ( gr.update(value=face_img), gr.update(value="face_id"), "✅ ส่งใบหน้าไปแถบ 2 แล้ว (ตั้งเป็น FaceID) — ไปแถบ '🎬 ฉาก/ท่าทาง' ตั้งค่าฉาก/ชุด/ท่า แล้วกด Generate", ) CSS = """ #gen-btn {height: 100%; font-size: 1.3rem; font-weight: 700;} .card {border-radius: 14px;} footer {visibility: hidden;} """ with gr.Blocks(css=CSS, theme=gr.themes.Soft(primary_hue="blue"), title="Character Studio") as demo: gr.Markdown("## 🎭 Character Studio — multi-model character generator (ZeroGPU)") # ---- shared model picker (ใช้ร่วมทั้งแถบ 1 และ 2) ---- ids_state = gr.State(model_ids(MODELS)) selected_id = gr.State(MODELS[0]["id"] if MODELS else None) with gr.Group(): gr.Markdown("### 🧩 เลือกโมเดล / Models — ใช้ร่วมกันทั้งแถบ 1 และ 2") model_gallery = gr.Gallery( value=gallery_items(MODELS), label=None, show_label=False, columns=3, height="auto", object_fit="cover", allow_preview=False, container=False, elem_classes="card", ) selected_md = gr.Markdown( f"**เลือก / Selected:** {MODELS[0]['label']}" if MODELS else "" ) reload_btn = gr.Button("🔄 Reload models", size="sm") reload_status = gr.Markdown("") with gr.Tabs(): with gr.Tab("👤 แถบ 1 · สร้างใบหน้า / Identity"): gr.Markdown("### 👤 แถบ 1 · สร้างใบหน้า/อัตลักษณ์ก่อน — เน้นโคลสอัพให้ได้หน้าที่ต้องการ แล้วส่งไปแถบ 2") gr.Markdown("กรอกลักษณะใบหน้า (ไทยได้) → กด สร้าง prompt → กด สร้างใบหน้า · ใช้โมเดลที่เลือกจากแถบ 2") with gr.Row(): f_gender = gr.Dropdown( label="เพศ", choices=[ ("หญิง / woman", "woman, 1girl, female, feminine"), ("ชาย / man", "man, 1boy, male, masculine"), ], value="woman, 1girl, female, feminine", multiselect=False, allow_custom_value=True) f_age = gr.Textbox(label="อายุ", placeholder="เช่น 22") f_ethnicity = gr.Textbox(label="เชื้อชาติ", placeholder="เช่น ไทย, เกาหลี") with gr.Row(): f_faceshape = gr.Dropdown( label="โครงหน้า (เลือกได้/พิมพ์เองได้)", choices=[ ("รูปไข่ / oval", "oval face"), ("กลม / round", "round face"), ("หัวใจ / heart", "heart-shaped face"), ("เหลี่ยม / square", "square face, defined jaw"), ("ยาว / long", "long face"), ("เรียว V / v-line", "v-line face, slim jaw"), ], value=None, multiselect=True, allow_custom_value=True) f_skin = gr.Dropdown( label="สีผิว (เลือกได้/พิมพ์เองได้)", choices=[ ("ขาว / fair", "fair skin"), ("ขาวอมชมพู / light", "light skin, rosy"), ("สองสี / olive", "olive skin"), ("แทน / tan", "tan skin"), ("แทนเข้ม / dark", "dark skin"), ], value=None, multiselect=True, allow_custom_value=True) f_eyecolor = gr.Dropdown( label="สีตา (เลือกได้/พิมพ์เองได้)", choices=[ ("น้ำตาล / brown", "brown eyes"), ("น้ำตาลเข้ม / dark brown", "dark brown eyes"), ("ดำ / black", "black eyes"), ("น้ำผึ้ง / hazel", "hazel eyes"), ("ฟ้า / blue", "blue eyes"), ("เขียว / green", "green eyes"), ("เทา / gray", "gray eyes"), ], value=None, multiselect=True, allow_custom_value=True) with gr.Row(): f_eyes = gr.Dropdown( label="ดวงตา (เลือกได้/พิมพ์เองได้)", choices=[ ("ตากลมโต / round", "big round eyes"), ("ตาอัลมอนด์ / almond", "almond eyes"), ("ตาชั้นเดียว / monolid", "monolid eyes"), ("ตาสองชั้น / double eyelid", "double eyelid"), ("ตาหางชี้ / upturned", "upturned eyes"), ("ตาหางตก / downturned", "downturned eyes"), ], value=None, multiselect=True, allow_custom_value=True) f_brow = gr.Dropdown( label="คิ้ว (เลือกได้/พิมพ์เองได้)", choices=[ ("คิ้วหนา / thick", "thick eyebrows"), ("คิ้วบาง / thin", "thin eyebrows"), ("คิ้วโก่ง / arched", "arched eyebrows"), ("คิ้วตรง / straight", "straight eyebrows"), ], value=None, multiselect=True, allow_custom_value=True) f_nose = gr.Dropdown( label="จมูก (เลือกได้/พิมพ์เองได้)", choices=[ ("จมูกโด่ง / pointed", "pointed nose, high nose bridge"), ("จมูกเล็ก / small", "small nose"), ("จมูกตรง / straight", "straight nose"), ("จมูกบาน / wide", "wide nose"), ], value=None, multiselect=True, allow_custom_value=True) with gr.Row(): f_mouth = gr.Dropdown( label="ปาก (เลือกได้/พิมพ์เองได้)", choices=[ ("ปากอิ่ม / full", "full lips"), ("ปากบาง / thin", "thin lips"), ("ปากเล็ก / small", "small lips"), ("ปากกระจับ / heart", "heart-shaped lips"), ("ยิ้มมุมปาก / slight smile", "slight smile"), ], value=None, multiselect=True, allow_custom_value=True) f_hair = gr.Dropdown( label="ทรงผม / สีผม (เลือกความยาว+สี · พิมพ์เองได้)", choices=[ ("ผมสั้น / short", "short hair"), ("บ๊อบ / bob", "bob cut"), ("ประบ่า / medium", "medium hair, shoulder-length"), ("ผมยาว / long", "long hair"), ("ยาวมาก / very long", "very long hair"), ("หางม้า / ponytail", "ponytail"), ("มัดมวย / bun", "hair bun"), ("ผมม้า / bangs", "blunt bangs"), ("หยิก / curly", "curly hair"), ("ตรง / straight", "straight hair"), ("ดำ / black", "black hair"), ("น้ำตาลเข้ม / dark brown", "dark brown hair"), ("น้ำตาล / brown", "brown hair"), ("บลอนด์ / blonde", "blonde hair"), ("ทอง / golden", "golden blonde hair"), ("แดง / red", "red hair"), ], value=None, multiselect=True, allow_custom_value=True) f_expr = gr.Dropdown( label="สีหน้า / อารมณ์ (เลือกได้/พิมพ์เองได้)", choices=[ ("ยิ้มอ่อน / soft smile", "soft smile"), ("ยิ้มกว้าง / grin", "happy grin, smiling"), ("นิ่ง / neutral", "neutral expression"), ("เซ็กซี่ / seductive", "seductive expression"), ("เศร้า / sad", "sad expression"), ("มองกล้อง / look at viewer", "looking at viewer, eye contact"), ], value=None, multiselect=True, allow_custom_value=True) face_build_btn = gr.Button("✨ สร้าง prompt ใบหน้า", variant="secondary") with gr.Row(): with gr.Column(scale=3): face_prompt = gr.Textbox(label="Prompt ใบหน้า (แก้ได้)", lines=2, placeholder="กดปุ่มด้านบน หรือพิมพ์เอง") with gr.Column(scale=1): face_gen_btn = gr.Button("🎨 สร้างใบหน้า (close-up)", variant="primary") face_output = gr.Image(label="ใบหน้าที่ได้", height=420, type="pil", elem_classes="card") face_status = gr.Markdown("") send_face_btn = gr.Button("➡️ ใช้ใบหน้านี้ในแถบ 2 (FaceID)", variant="secondary") send_note = gr.Markdown("") with gr.Tab("🎬 แถบ 2 · สร้างฉาก/ท่าทาง / Scene"): with gr.Row(): prompt = gr.Textbox( label="Edit Prompt", lines=2, scale=4, placeholder="✦ เลือกโมเดลแล้วพิมพ์ prompt / Choose a model and type the prompt", ) gen_btn = gr.Button("Generate", variant="primary", scale=1, elem_id="gen-btn") # ---- Prompt Builder: fill blanks instead of writing sentences ---- with gr.Accordion("📝 ตัวช่วยสร้าง prompt / Prompt Builder — กรอกช่อง ไม่ต้องเขียนประโยค", open=False): gr.Markdown("กรอกเฉพาะช่องที่ต้องการ (เป็นภาษาไทยได้) แล้วกดปุ่มด้านล่าง " "ระบบจะประกอบเป็น prompt ให้ในช่องด้านบน · เว้นว่างช่องที่ไม่ใช้ได้") with gr.Row(): b_subject = gr.Textbox(label="ใคร / ตัวละคร", value="ผู้หญิง") b_age = gr.Textbox(label="อายุ", placeholder="เช่น 25") b_ethnicity = gr.Textbox(label="เชื้อชาติ / สัญชาติ", placeholder="เช่น เกาหลี, ไทย") with gr.Row(): b_body = gr.Dropdown( label="รูปร่าง / สัดส่วน (เลือกได้หลายอัน · พิมพ์เองได้)", choices=[ ("ผอมเพรียว / slim", "slim, slender figure"), ("ตัวเล็กบอบบาง / petite", "petite, small frame"), ("ทรงนาฬิกาทราย / hourglass", "hourglass figure, narrow waist"), ("อวบอั๋นเว้าโค้ง / curvy", "curvy, voluptuous"), ("ฟิตมีกล้าม / athletic", "athletic, toned, fit body"), ("อึ๋มอกใหญ่ / busty", "busty, large breasts"), ("ท้วมอวบ / chubby", "chubby, plump"), ("สูงขายาวทรงนางแบบ / tall model", "tall, long legs, model body"), ("สะโพกผาย / wide hips", "wide hips"), ("ต้นขาหนา / thick thighs", "thick thighs"), ("เอวเล็ก / slim waist", "slim waist"), ("อกเล็ก / small breasts", "small breasts"), ("ทรงผู้ใหญ่ / mature", "mature body"), ], value=None, multiselect=True, allow_custom_value=True) b_hair = gr.Textbox(label="ทรงผม / สีผม", placeholder="เช่น ผมยาวสีดำ, ผมบลอนด์") b_eyes = gr.Textbox(label="สีตา", placeholder="เช่น ตาสีน้ำตาล") with gr.Row(): b_skin = gr.Textbox(label="สีผิว", placeholder="เช่น ผิวขาว, ผิวสองสี, ผิวแทนทอง, ผิวเนียน") b_face = gr.Textbox(label="โครงหน้า", placeholder="เช่น หน้าเรียว, หน้ารูปไข่, หน้ากลม, คางแหลม") with gr.Row(): b_outfit = gr.Textbox(label="ชุด / เสื้อผ้า", placeholder="เช่น เดรสสีขาว, ชุดนักเรียน") b_pose = gr.Dropdown( label="ท่าโพส (เลือกผสมได้ · พิมพ์เองได้)", choices=[ ("ยืนตรง / standing", "standing"), ("ยืนมือเท้าเอว / hands on hips", "standing, hands on hips"), ("ยืนเอียงสะโพก / contrapposto", "standing, contrapposto pose"), ("ยืนพิงกำแพง / leaning on wall", "leaning against wall"), ("นั่งเก้าอี้ / sitting (chair)", "sitting on chair"), ("นั่งพื้น / sitting (floor)", "sitting on floor"), ("นั่งไขว่ห้าง / crossed legs", "sitting, crossed legs"), ("นั่งชันเข่า / knees up", "sitting, knees up, hugging knees"), ("คุกเข่า / kneeling", "kneeling"), ("นอนหงาย / lying on back", "lying on back"), ("นอนคว่ำ / on stomach", "lying on stomach"), ("นอนตะแคง / on side", "lying on side"), ("เดิน / walking", "walking"), ("วิ่ง / running", "running"), ("มือรวบผม / hand in hair", "hand in own hair, arm up"), ("กอดอก / arms crossed", "crossed arms"), ("โบกมือ / waving", "waving hand"), ], value=None, multiselect=True, allow_custom_value=True) b_expr = gr.Textbox(label="สีหน้า / อารมณ์", placeholder="เช่น ยิ้มอ่อนๆ") with gr.Row(): b_scene = gr.Textbox(label="สถานที่ / ฉาก", placeholder="เช่น คาเฟ่, ริมทะเล, ในสวน") b_light = gr.Textbox(label="แสง", placeholder="เช่น แสงเช้านุ่มๆ, แสงสตูดิโอ") b_shot = gr.Dropdown( label="มุมกล้อง / ช็อต (เลือกได้หลายอัน · พิมพ์เองได้)", choices=[ ("โคลสอัพใบหน้า / close-up", "close-up portrait, face focus, headshot"), ("ครึ่งตัวบน / upper body", "upper body shot, bust"), ("ครึ่งตัว เอวขึ้นไป / half body", "half body shot, waist up"), ("เต็มตัว หัวถึงเท้า / full body", "full body shot, head to toe, full body visible"), ("ระยะไกล เห็นรอบตัว / wide shot", "wide shot, full body, distant, environmental"), ("มุมเงย / low angle", "from below, low angle shot"), ("มุมก้ม / high angle", "from above, high angle shot"), ("มุมข้าง โปรไฟล์ / side", "from side, profile view"), ("มุมหลัง / from behind", "from behind, back view"), ("เซลฟี่ / selfie", "selfie, pov, arm extended"), ], value=None, multiselect=True, allow_custom_value=True) with gr.Row(): b_pose_img = gr.Image( label="📷 รูปอ้างอิงท่าโพส (ใส่รูป → ล็อกท่าตามรูปด้วย ControlNet OpenPose · " "หน้า/ชุด/ฉาก ยังคุมด้วยช่องด้านบน · เว้นว่าง = ใช้ข้อความท่าโพส)", type="pil", sources=["upload"], height=240) build_btn = gr.Button("✨ สร้าง prompt → ใส่ในช่องด้านบน", variant="secondary") with gr.Row(equal_height=False): # ---- left: model picker ---- with gr.Column(scale=1): mode_radio = gr.Radio( choices=modes_for(MODELS, MODELS[0]["id"]) if MODELS else [], value="txt2img", label="โหมดรูปต้นแบบ / Input mode", ) translator = gr.Radio( choices=[("ปิด / Off", "off"), ("NLLB-200 (เร็ว)", "nllb"), ("Typhoon 2 (ไทยแน่น)", "typhoon")], value="typhoon", label="แปลไทย→อังกฤษ / Auto-translate (พิมพ์ไทยได้เลย)", ) # ---- right: output ---- with gr.Column(scale=1): output = gr.Image(label="Generated Image", height=560, elem_classes="card") status = gr.Markdown("") # ---- advanced ---- with gr.Accordion("Advanced Settings", open=False): with gr.Row(): with gr.Column(): ref_image = gr.Image(label="Input image (รูปต้นแบบ)", type="pil", height=240) ip_scale = gr.Slider(0.0, 1.5, value=0.7, step=0.05, label="Reference strength (IP-Adapter / FaceID)") denoise = gr.Slider(0.1, 1.0, value=0.65, step=0.01, label="Denoise strength (img2img · ต่ำ = อิงรูปมาก)") with gr.Column(): negative_prompt = gr.Textbox(label="Negative prompt", lines=2) with gr.Row(): steps = gr.Slider(1, 50, value=28, step=1, label="Steps") guidance = gr.Slider(0.0, 15.0, value=6.5, step=0.1, label="Guidance (CFG)") with gr.Row(): width = gr.Slider(384, 1280, value=512, step=64, label="Width") height = gr.Slider(384, 1280, value=768, step=64, label="Height") with gr.Row(): seed = gr.Number(value=-1, label="Seed (-1 = random)", precision=0) randomize = gr.Checkbox(value=True, label="Randomize seed") # ---- wiring ---- model_gallery.select( on_gallery_select, inputs=[ids_state], outputs=[selected_id, mode_radio, prompt, negative_prompt, steps, guidance, width, height, selected_md], ) reload_btn.click( reload_registry, outputs=[model_gallery, ids_state, selected_id, reload_status], ) build_btn.click( build_prompt, inputs=[b_subject, b_age, b_ethnicity, b_skin, b_face, b_body, b_hair, b_eyes, b_outfit, b_pose, b_expr, b_scene, b_light, b_shot], outputs=prompt, ) gen_inputs = [selected_id, mode_radio, prompt, negative_prompt, ref_image, steps, guidance, denoise, ip_scale, width, height, seed, randomize, translator, b_pose_img] gen_btn.click(generate, inputs=gen_inputs, outputs=[output, seed, status]) prompt.submit(generate, inputs=gen_inputs, outputs=[output, seed, status]) # ---- Tab 1 (identity) wiring ---- face_build_btn.click( build_face_prompt, inputs=[f_gender, f_age, f_ethnicity, f_faceshape, f_skin, f_eyecolor, f_eyes, f_brow, f_nose, f_mouth, f_hair, f_expr], outputs=face_prompt, ) face_gen_btn.click( generate_face, inputs=[selected_id, face_prompt, translator], outputs=[face_output, face_status], ) send_face_btn.click( send_face_to_scene, inputs=[face_output], outputs=[ref_image, mode_radio, send_note], ) if __name__ == "__main__": # allowed_paths lets Gradio serve the local model preview thumbnails. demo.queue(max_size=12).launch(allowed_paths=["previews"])