Spaces:
Running on Zero
Running on Zero
Tab1: gender -> dropdown (woman/1girl strong tag), auto male-negatives when female so short-hair/jaw dont flip to male
499adbc verified | """ | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| 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" | |
| 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"]) | |