pormungtai's picture
Tab1: gender -> dropdown (woman/1girl strong tag), auto male-negatives when female so short-hair/jaw dont flip to male
499adbc verified
Raw
History Blame
36.8 kB
"""
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"])