mamungtai-sat pormungtai commited on
Commit
ec440e5
·
1 Parent(s): f7097b9

Split into 2 tabs: Tab1 Identity (close-up face builder + send-to-scene as FaceID), Tab2 = existing scene builder (#39)

Browse files

- Split into 2 tabs: Tab1 Identity (close-up face builder + send-to-scene as FaceID), Tab2 = existing scene builder (899f185fd8e1bd5177663d04e88316021b2ad93f)


Co-authored-by: pormungtailaw <pormungtai@users.noreply.huggingface.co>

Files changed (1) hide show
  1. app.py +255 -147
app.py CHANGED
@@ -219,6 +219,63 @@ def build_prompt(subject, age, ethnicity, skin, face, body, hair, eyes, outfit,
219
  # ---------------------------------------------------------------------------
220
  # Layout (mirrors the FLUX LoRA DLC reference UI)
221
  # ---------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  CSS = """
223
  #gen-btn {height: 100%; font-size: 1.3rem; font-weight: 700;}
224
  .card {border-radius: 14px;}
@@ -228,157 +285,188 @@ footer {visibility: hidden;}
228
  with gr.Blocks(css=CSS, theme=gr.themes.Soft(primary_hue="blue"),
229
  title="Character Studio") as demo:
230
  gr.Markdown("## 🎭 Character Studio — multi-model character generator (ZeroGPU)")
231
-
232
- with gr.Row():
233
- prompt = gr.Textbox(
234
- label="Edit Prompt", lines=2, scale=4,
235
- placeholder="✦ เลือกโมเดลแล้วพิมพ์ prompt / Choose a model and type the prompt",
236
- )
237
- gen_btn = gr.Button("Generate", variant="primary", scale=1, elem_id="gen-btn")
238
-
239
- # ---- Prompt Builder: fill blanks instead of writing sentences ----
240
- with gr.Accordion("📝 ตัวช่วยสร้า prompt / Prompt Builder — กรอกช่อง ไม่ตองียนปะโยค",
241
- open=False):
242
- gr.Markdown("กรอกเฉพาะช่องท้องก (ป็ภาษาไทยได้) แลวกดปุ่มด้่าง "
243
- "ระบบจะประกอบเป็น prompt ให้ในช่องด้านบน · เว้นว่างช่องที่ไม่ใช้ได้")
244
- with gr.Row():
245
- b_subject = gr.Textbox(label="ร / ตัละคร", value="ผูหญิง")
246
- b_age = gr.Textbox(label="อายุ", placeholder="เช่น 25")
247
- b_ethnicity = gr.Textbox(label="เชื้อชาติ / สัญชาติ", placeholder="เช่น เกาหลี, ไทย")
248
- with gr.Row():
249
- b_body = gr.Dropdown(
250
- label="รูปร่ / สัดส(เลือกได้หลายอ· พมพ์เอได้)",
251
- choices=[
252
- ("ผอมเพรียว / slim", "slim, slender figure"),
253
- ("ต���วเล็กบอบบาง / petite", "petite, small frame"),
254
- ("ทรงนาฬิกาทราย / hourglass", "hourglass figure, narrow waist"),
255
- ("อวบอั๋นเว้าโค้ง / curvy", "curvy, voluptuous"),
256
- ("ฟิตมีกล้า / athletic", "athletic, toned, fit body"),
257
- ("อึ๋มอกให / busty", "busty, large breasts"),
258
- ("ท้วมอวบ / chubby", "chubby, plump"),
259
- ("สูงขายาวทรงนาแบ / tall model", "tall, long legs, model body"),
260
- ("สะโพกผาย / wide hips", "wide hips"),
261
- ("นขหนา / thick thighs", "thick thighs"),
262
- ("เอวเล็ก / slim waist", "slim waist"),
263
- ("อกเล็ก / small breasts", "small breasts"),
264
- ("ทรงผู้ใหญ่ / mature", "mature body"),
265
- ],
266
- value=None, multiselect=True, allow_custom_value=True)
267
- b_hair = gr.Textbox(label="ทรงผม / สีผม", placeholder="เช่น ผมยาวสีดำ, ผมบลอนด์")
268
- b_eyes = gr.Textbox(label="สีตา", placeholder="เช่น ตาสีน้ำตาล")
269
- with gr.Row():
270
- b_skin = gr.Textbox(label="สีผิว", placeholder="เช่น ผิวขาว, ผิวสองสี, ผิวแทนทอง, ผิวเนียน")
271
- b_face = gr.Textbox(label="โครงหน้า", placeholder="เช่น หน้าเรียว, หน้ารูปไข่, หน้ากลม, คางแหลม")
272
- with gr.Row():
273
- b_outfit = gr.Textbox(label="ชุด / เสื้อผ้า", placeholder="เช่น เดรสสีขาว, ชุดนักเรียน")
274
- b_pose = gr.Dropdown(
275
- label="ท่าโพส (เลือกผสมได้ · พิมพ์เองได้)",
276
- choices=[
277
- ("ยืนตรง / standing", "standing"),
278
- ("ยืนมือเท้าเอว / hands on hips", "standing, hands on hips"),
279
- ("ยืนเอียงสะโพก / contrapposto", "standing, contrapposto pose"),
280
- ("ยืนพิงกำแพง / leaning on wall", "leaning against wall"),
281
- ("นั่งเก้าอี้ / sitting (chair)", "sitting on chair"),
282
- ("นั่งพื้น / sitting (floor)", "sitting on floor"),
283
- ("นั่งไขว่ห้าง / crossed legs", "sitting, crossed legs"),
284
- ("นั่งชันเข่า / knees up", "sitting, knees up, hugging knees"),
285
- ("คุกเข่า / kneeling", "kneeling"),
286
- ("นอนหงาย / lying on back", "lying on back"),
287
- ("นอนคว่ำ / on stomach", "lying on stomach"),
288
- ("นอนตะแคง / on side", "lying on side"),
289
- ("เดิน / walking", "walking"),
290
- ("วิ่ง / running", "running"),
291
- ("มือรวบผม / hand in hair", "hand in own hair, arm up"),
292
- ("กอดอก / arms crossed", "crossed arms"),
293
- ("โบกมือ / waving", "waving hand"),
294
- ],
295
- value=None, multiselect=True, allow_custom_value=True)
296
- b_expr = gr.Textbox(label="สีหน้า / อารมณ์", placeholder="เช่น ยิ้มอ่อนๆ")
297
- with gr.Row():
298
- b_scene = gr.Textbox(label="สถานที่ / ฉาก", placeholder="เช่น คาเฟ่, ริมทะเล, ในสวน")
299
- b_light = gr.Textbox(label="แสง", placeholder="เช่น แสงเช้านุ่มๆ, แสงสตูดิโอ")
300
- b_shot = gr.Dropdown(
301
- label="มุมกล้อง / ช็อต (เลือกได้หลายอัน · พิมพ์เองได้)",
302
- choices=[
303
- ("โคลสอัพใบหน้า / close-up", "close-up portrait, face focus, headshot"),
304
- ("ครึ่งตัวบน / upper body", "upper body shot, bust"),
305
- ("ครึ่งตัว เอวขึ้นไป / half body", "half body shot, waist up"),
306
- ("เต็มตัว หัวถึงเท้า / full body", "full body shot, head to toe, full body visible"),
307
- ("ระยะไกล เห็นรอบตัว / wide shot", "wide shot, full body, distant, environmental"),
308
- ("มุมเงย / low angle", "from below, low angle shot"),
309
- ("มุมก้ม / high angle", "from above, high angle shot"),
310
- ("มุมข้าง โปรไฟล์ / side", "from side, profile view"),
311
- ("มุมหลัง / from behind", "from behind, back view"),
312
- ("เซลฟี่ / selfie", "selfie, pov, arm extended"),
313
- ],
314
- value=None, multiselect=True, allow_custom_value=True)
315
- with gr.Row():
316
- b_pose_img = gr.Image(
317
- label="📷 รูปอ้างอิงท่าโพส (ใส่รูป → ล็อกท่าตามรูปด้วย ControlNet OpenPose · "
318
- "หน้า/ชุด/ฉาก ยังคุมด้วยช่องด้านบน · เว้นว่าง = ใช้ข้อความท่าโพส)",
319
- type="pil", sources=["upload"], height=240)
320
- build_btn = gr.Button("✨ สร้าง prompt → ใส่ในช่องด้านบน", variant="secondary")
321
-
322
- # State: ordered list of model ids (matches gallery order) + current selection.
323
- ids_state = gr.State(model_ids(MODELS))
324
- selected_id = gr.State(MODELS[0]["id"] if MODELS else None)
325
-
326
- with gr.Row(equal_height=False):
327
- # ---- left: model picker ----
328
- with gr.Column(scale=1):
329
- with gr.Group():
330
- gr.Markdown("### 🧩 เลือกโมเดล / Models")
331
- model_gallery = gr.Gallery(
332
- value=gallery_items(MODELS),
333
- label=None, show_label=False, columns=2, height="auto",
334
- object_fit="cover", allow_preview=False, container=False,
335
- elem_classes="card",
336
  )
337
- selected_md = gr.Markdown(
338
- f"**เลือก / Selected:** {MODELS[0]['label']}" if MODELS else ""
339
- )
340
- reload_btn = gr.Button("🔄 Reload models", size="sm")
341
- reload_status = gr.Markdown("")
342
-
343
- mode_radio = gr.Radio(
344
- choices=modes_for(MODELS, MODELS[0]["id"]) if MODELS else [],
345
- value="txt2img",
346
- label="โหมดรูปต้แบบ / Input mode",
347
- )
348
-
349
- translator = gr.Radio(
350
- choices=[("ปิด / Off", "off"),
351
- ("NLLB-200 (เร็ว)", "nllb"),
352
- ("Typhoon 2 (ไทยแน่น)", "typhoon")],
353
- value="typhoon",
354
- label="แปลไย→อังกฤษ / Auto-translate (พิมพ์ไทยได้เลย)",
355
- )
356
-
357
- # ---- right: output ----
358
- with gr.Column(scale=1):
359
- output = gr.Image(label="Generated Image", height=560, elem_classes="card")
360
- status = gr.Markdown("")
361
-
362
- # ---- advanced ----
363
- with gr.Accordion("Advanced Settings", open=False):
364
- with gr.Row():
365
- with gr.Column():
366
- ref_image = gr.Image(label="Input image (รูปต้นแบบ)", type="pil", height=240)
367
- ip_scale = gr.Slider(0.0, 1.5, value=0.7, step=0.05,
368
- label="Reference strength (IP-Adapter / FaceID)")
369
- denoise = gr.Slider(0.1, 1.0, value=0.65, step=0.01,
370
- label="Denoise strength (img2img · ต่ำ = อิงรูปมาก)")
371
- with gr.Column():
372
- negative_prompt = gr.Textbox(label="Negative prompt", lines=2)
373
  with gr.Row():
374
- steps = gr.Slider(1, 50, value=28, step=1, label="Steps")
375
- guidance = gr.Slider(0.0, 15.0, value=6.5, step=0.1, label="Guidance (CFG)")
376
  with gr.Row():
377
- width = gr.Slider(384, 1280, value=512, step=64, label="Width")
378
- height = gr.Slider(384, 1280, value=768, step=64, label="Height")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  with gr.Row():
380
- seed = gr.Number(value=-1, label="Seed (-1 = random)", precision=0)
381
- randomize = gr.Checkbox(value=True, label="Randomize seed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
  # ---- wiring ----
384
  model_gallery.select(
@@ -404,6 +492,26 @@ with gr.Blocks(css=CSS, theme=gr.themes.Soft(primary_hue="blue"),
404
  prompt.submit(generate, inputs=gen_inputs, outputs=[output, seed, status])
405
 
406
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  if __name__ == "__main__":
408
  # allowed_paths lets Gradio serve the local model preview thumbnails.
409
  demo.queue(max_size=12).launch(allowed_paths=["previews"])
 
219
  # ---------------------------------------------------------------------------
220
  # Layout (mirrors the FLUX LoRA DLC reference UI)
221
  # ---------------------------------------------------------------------------
222
+ def build_face_prompt(gender, age, ethnicity, faceshape, skin, eyecolor,
223
+ eyes, brow, nose, mouth, hair, expr):
224
+ """Assemble Tab-1 face fields into an identity-focused prompt (Thai ok)."""
225
+ who = _field(gender) or "ผู้หญิง"
226
+ e, a = _field(ethnicity), _field(age)
227
+ if e:
228
+ who = f"{e} {who}"
229
+ if a:
230
+ who = f"{who} อายุ {a} ปี"
231
+ parts = [who]
232
+ for v in (faceshape, skin, eyecolor, eyes, brow, nose, mouth, hair, expr):
233
+ s = _field(v)
234
+ if s:
235
+ parts.append(s)
236
+ return ", ".join(parts) + ", detailed face, beautiful detailed eyes, detailed skin"
237
+
238
+
239
+ @spaces.GPU(duration=120)
240
+ def generate_face(model_id, prompt, translator):
241
+ """Tab 1: generate a close-up identity portrait to lock the face first."""
242
+ models = load_models()
243
+ cfg = pm.get_model(models, model_id)
244
+ if cfg is None:
245
+ raise gr.Error("ไม่พบโมเดล — กด Reload models ในแถบ 2 / model not found")
246
+ if not (prompt and str(prompt).strip()):
247
+ raise gr.Error("กรอกช่องใบหน้าแล้วกด '✨ สร้าง prompt ใบหน้า' ก่อน")
248
+ import random as _r
249
+ seed = _r.randint(0, MAX_SEED)
250
+ p = pm.translate_prompt(prompt, translator)
251
+ # force a close-up identity framing regardless of translator wording
252
+ p = "RAW photo, close-up portrait, face focus, headshot, looking at viewer, " + p
253
+ neg = cfg.get("negative_prompt", "")
254
+ try:
255
+ img = pm.run_generation(
256
+ cfg=cfg, mode="txt2img", prompt=p, negative_prompt=neg, ref_image=None,
257
+ steps=int(cfg.get("default_steps", 30)),
258
+ guidance=float(cfg.get("default_guidance", 5.0)),
259
+ denoise=0.4, ip_scale=0.7, width=512, height=640, seed=seed,
260
+ )
261
+ except Exception as e: # noqa
262
+ traceback.print_exc()
263
+ raise gr.Error(str(e))
264
+ return img, f"✅ ใบหน้า · {cfg['label']} · seed {seed}"
265
+
266
+
267
+ def send_face_to_scene(face_img):
268
+ """Copy the Tab-1 face into Tab-2's reference image + switch to FaceID."""
269
+ if face_img is None:
270
+ return gr.update(), gr.update(), "⚠️ ยังไม่มีใบหน้า — กด 'สร้างใบหน้า' ก่อน"
271
+ return (
272
+ gr.update(value=face_img),
273
+ gr.update(value="face_id"),
274
+ "✅ ส่งใบหน้าไปแถบ 2 แล้ว (ตั้งเป็น FaceID) — ไปแถบ '🎬 ฉาก/ท่าทาง' ตั้งค่าฉาก/ชุด/ท่า แล้วกด Generate",
275
+ )
276
+
277
+
278
+
279
  CSS = """
280
  #gen-btn {height: 100%; font-size: 1.3rem; font-weight: 700;}
281
  .card {border-radius: 14px;}
 
285
  with gr.Blocks(css=CSS, theme=gr.themes.Soft(primary_hue="blue"),
286
  title="Character Studio") as demo:
287
  gr.Markdown("## 🎭 Character Studio — multi-model character generator (ZeroGPU)")
288
+ with gr.Tabs():
289
+ with gr.Tab("👤 แถบ 1 · สร้างใบหน้า / Identity"):
290
+ gr.Markdown("### 👤 แถบ 1 · สร้างใบหน้า/อัตลักษณ์ก่อน — เน้นโคลสอัพให้ได้หน้าที่ต้องการ แล้วส่งไปแถบ 2")
291
+ gr.Markdown("กรอกลักษณะใบหน้า (ไทยได้) กด สร้าง prompt → กด สร้างใบหน้า · ใช้โมเดลที่เลือกจากแถบ 2")
292
+ with gr.Row():
293
+ f_gender = gr.Textbox(label="เพศ", value="ผู้หญิง")
294
+ f_age = gr.Textbox(label="อายุ", placeholder="เช่น 22")
295
+ f_ethnicity = gr.Textbox(label="เชื้อชาติ", placeholder="เช่น ไทย, เกาหลี")
296
+ with gr.Row():
297
+ f_faceshape = gr.Textbox(label="โคงหน้า", placeholder="เช่ หนียว, ูปไข่, กลม")
298
+ f_skin = gr.Textbox(label="สีผิว", placeholder="เช่น ผิวขาว, สองสี, แทน")
299
+ f_eyecolor = gr.Textbox(label="ีตา", placeholder="ช่ำตาล, ดำ")
300
+ with gr.Row():
301
+ f_eyes = gr.Textbox(label="ดวงตา", placeholder="เช่น ตากลมโต, ตาชั้นเดียว")
302
+ f_brow = gr.Textbox(label="คิ้ว", placeholder="เช่น คิวเข้ม, ค้วโก่ง")
303
+ f_nose = gr.Textbox(label="จมูก", placeholder="เช่น จมูกโด่ง, เล็ก")
304
+ with gr.Row():
305
+ f_mouth = gr.Textbox(label="ปาก", placeholder="เช่น ปากอิ่ม, ยิ้มมุมปาก")
306
+ f_hair = gr.Textbox(label="ทรงผม / สีผม", placeholder="เช่น ผมยาวสีดำ")
307
+ f_expr = gr.Textbox(label="สีหน้า / อารมณ์", placeholder="เช่น ยิ้ม่อๆ, ง")
308
+ face_build_btn = gr.Button("✨ สร้าง prompt ใบหน้า", variant="secondary")
309
+ with gr.Row():
310
+ with gr.Column(scale=3):
311
+ face_prompt = gr.Textbox(label="Prompt ใบห(แก้ได้)", lines=2, placeholder="กดปุ่มด้านบน หรือพิมพ์เอง")
312
+ with gr.Column(scale=1):
313
+ face_gen_btn = gr.Button("🎨 สร้างใบหน้า (close-up)", variant="primary")
314
+ face_output = gr.Image(label="ใน้าทีได้", height=420, type="pil", elem_classes="card")
315
+ face_status = gr.Markdown("")
316
+ send_face_btn = gr.Button("➡️ ใช้ใบหนี้ใน2 (FaceID)", variant="secondary")
317
+ send_note = gr.Markdown("")
318
+ with gr.Tab("🎬 แถบ 2 · สร้างฉก/ท่าทาง / Scene"):
319
+
320
+ with gr.Row():
321
+ prompt = gr.Textbox(
322
+ label="Edit Prompt", lines=2, scale=4,
323
+ placeholder="✦ เลือกโมเดลแล้วพิมพ์ prompt / Choose a model and type the prompt",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  )
325
+ gen_btn = gr.Button("Generate", variant="primary", scale=1, elem_id="gen-btn")
326
+
327
+ # ---- Prompt Builder: fill blanks instead of writing sentences ----
328
+ with gr.Accordion("📝 ตัวช่วยสร้าง prompt / Prompt Builder — กรอกช่อง ไม่ต้องเขียนประโยค",
329
+ open=False):
330
+ gr.Markdown("กรอกเฉพาะช่องที่ต้องการ (เป็นภาษาไทยได้) แล้วกดปุ่มด้านล่าง "
331
+ "ระบบจะประกอบเป็น prompt ให้ในช่องด้านบน · เว้นว่างช่องที่ไม่ใช้ได้")
332
+ with gr.Row():
333
+ b_subject = gr.Textbox(label="ใคร / ตัวละคร", value="ผู้หญิง")
334
+ b_age = gr.Textbox(label="อายุ", placeholder="เช่25")
335
+ b_ethnicity = gr.Textbox(label="เชื้อชาติ / สัญชาติ", placeholder="เช่น เกาหลี, ไทย")
336
+ with gr.Row():
337
+ b_body = gr.Dropdown(
338
+ label="รูร่าง / สัดส่วน (เลือกได้หลายอัน · พิมพ์เองได้)",
339
+ choices=[
340
+ ("ผอมเพรียว / slim", "slim, slender figure"),
341
+ ("ตัวเล็กบอบบาง / petite", "petite, small frame"),
342
+ ("ทนาฬิาทราย / hourglass", "hourglass figure, narrow waist"),
343
+ ("อวบอั๋นเว้าโค้ง / curvy", "curvy, voluptuous"),
344
+ ("ฟิตมีกล้าม / athletic", "athletic, toned, fit body"),
345
+ ("อึ๋มอกใหญ่ / busty", "busty, large breasts"),
346
+ ("ท้วมอวบ / chubby", "chubby, plump"),
347
+ ("สูงขายาวทรงนางแบบ / tall model", "tall, long legs, model body"),
348
+ ("สะโพกผาย / wide hips", "wide hips"),
349
+ ("ต้นขาหนา / thick thighs", "thick thighs"),
350
+ ("เอวเล็ก / slim waist", "slim waist"),
351
+ ("อกเล็ก / small breasts", "small breasts"),
352
+ ("ทรงผู้ใหญ่ / mature", "mature body"),
353
+ ],
354
+ value=None, multiselect=True, allow_custom_value=True)
355
+ b_hair = gr.Textbox(label="ทรงผม / สีผม", placeholder="เช่น ผม��าวสีดำ, ผมบลอนด์")
356
+ b_eyes = gr.Textbox(label="สีตา", placeholder="เช่น ตาสีน้ำตาล")
 
 
 
 
357
  with gr.Row():
358
+ b_skin = gr.Textbox(label="สีผิว", placeholder="เช่น ผิวขาว, ผิวสองสี, ผิวแทนทอง, ผิวเนียน")
359
+ b_face = gr.Textbox(label="โครงหน้า", placeholder="เช่น หน้าเรียว, หน้ารูปไข่, หน้ากลม, คางแหลม")
360
  with gr.Row():
361
+ b_outfit = gr.Textbox(label="ชุด / เสื้อผ้า", placeholder="เช่น เดรสสีขาว, ชุดนักเรียน")
362
+ b_pose = gr.Dropdown(
363
+ label="ท่าโพส (เลือกผสมได้ · พิมพ์เองได้)",
364
+ choices=[
365
+ ("ยืนตรง / standing", "standing"),
366
+ ("ยืนมือเท้าเอว / hands on hips", "standing, hands on hips"),
367
+ ("ยืนเอียงสะโพก / contrapposto", "standing, contrapposto pose"),
368
+ ("ยืนพิงกำแพง / leaning on wall", "leaning against wall"),
369
+ ("นั่งเก้าอี้ / sitting (chair)", "sitting on chair"),
370
+ ("นั่งพื้น / sitting (floor)", "sitting on floor"),
371
+ ("นั่งไขว่ห้าง / crossed legs", "sitting, crossed legs"),
372
+ ("นั่งชันเข่า / knees up", "sitting, knees up, hugging knees"),
373
+ ("คุกเข่า / kneeling", "kneeling"),
374
+ ("นอนหงาย / lying on back", "lying on back"),
375
+ ("นอนคว่ำ / on stomach", "lying on stomach"),
376
+ ("นอนตะแคง / on side", "lying on side"),
377
+ ("เดิน / walking", "walking"),
378
+ ("วิ่ง / running", "running"),
379
+ ("มือรวบผม / hand in hair", "hand in own hair, arm up"),
380
+ ("กอดอก / arms crossed", "crossed arms"),
381
+ ("โบกมือ / waving", "waving hand"),
382
+ ],
383
+ value=None, multiselect=True, allow_custom_value=True)
384
+ b_expr = gr.Textbox(label="สีหน้า / อารมณ์", placeholder="เช่น ยิ้มอ่อนๆ")
385
  with gr.Row():
386
+ b_scene = gr.Textbox(label="สถานที่ / ฉาก", placeholder="เช่น คาเฟ่, ริมทะเล, ในสวน")
387
+ b_light = gr.Textbox(label="แสง", placeholder="เช่น แสงเช้านุ่มๆ, แสงสตูดิโอ")
388
+ b_shot = gr.Dropdown(
389
+ label="มุมกล้อง / ช็อต (เลือกได้หลายอัน · พิมพ์เองได้)",
390
+ choices=[
391
+ ("โคลสอัพใบหน้า / close-up", "close-up portrait, face focus, headshot"),
392
+ ("ครึ่งตัวบน / upper body", "upper body shot, bust"),
393
+ ("ครึ่งตัว เอวขึ้นไป / half body", "half body shot, waist up"),
394
+ ("เต็มตัว หัวถึงเท้า / full body", "full body shot, head to toe, full body visible"),
395
+ ("ระยะไกล เห็นรอบตัว / wide shot", "wide shot, full body, distant, environmental"),
396
+ ("มุมเงย / low angle", "from below, low angle shot"),
397
+ ("มุมก้ม / high angle", "from above, high angle shot"),
398
+ ("มุมข้าง โปรไฟล์ / side", "from side, profile view"),
399
+ ("มุมหลัง / from behind", "from behind, back view"),
400
+ ("เซลฟี่ / selfie", "selfie, pov, arm extended"),
401
+ ],
402
+ value=None, multiselect=True, allow_custom_value=True)
403
+ with gr.Row():
404
+ b_pose_img = gr.Image(
405
+ label="📷 รูปอ้างอิงท่าโพส (ใส่รูป → ล็อกท่าตามรูปด้วย ControlNet OpenPose · "
406
+ "หน้า/ชุด/ฉาก ยังคุมด้วยช่องด้านบน · เว้นว่าง = ใช้ข้อความท่าโพส)",
407
+ type="pil", sources=["upload"], height=240)
408
+ build_btn = gr.Button("✨ สร้าง prompt → ใส่ในช่องด้านบน", variant="secondary")
409
+
410
+ # State: ordered list of model ids (matches gallery order) + current selection.
411
+ ids_state = gr.State(model_ids(MODELS))
412
+ selected_id = gr.State(MODELS[0]["id"] if MODELS else None)
413
+
414
+ with gr.Row(equal_height=False):
415
+ # ---- left: model picker ----
416
+ with gr.Column(scale=1):
417
+ with gr.Group():
418
+ gr.Markdown("### 🧩 เลือกโมเดล / Models")
419
+ model_gallery = gr.Gallery(
420
+ value=gallery_items(MODELS),
421
+ label=None, show_label=False, columns=2, height="auto",
422
+ object_fit="cover", allow_preview=False, container=False,
423
+ elem_classes="card",
424
+ )
425
+ selected_md = gr.Markdown(
426
+ f"**เลือก / Selected:** {MODELS[0]['label']}" if MODELS else ""
427
+ )
428
+ reload_btn = gr.Button("🔄 Reload models", size="sm")
429
+ reload_status = gr.Markdown("")
430
+
431
+ mode_radio = gr.Radio(
432
+ choices=modes_for(MODELS, MODELS[0]["id"]) if MODELS else [],
433
+ value="txt2img",
434
+ label="โหมดรูปต้นแบบ / Input mode",
435
+ )
436
+
437
+ translator = gr.Radio(
438
+ choices=[("ปิด / Off", "off"),
439
+ ("NLLB-200 (เร็ว)", "nllb"),
440
+ ("Typhoon 2 (ไทยแน่น)", "typhoon")],
441
+ value="typhoon",
442
+ label="แปลไทย→อังกฤษ / Auto-translate (พิมพ์ไทยได้เลย)",
443
+ )
444
+
445
+ # ---- right: output ----
446
+ with gr.Column(scale=1):
447
+ output = gr.Image(label="Generated Image", height=560, elem_classes="card")
448
+ status = gr.Markdown("")
449
+
450
+ # ---- advanced ----
451
+ with gr.Accordion("Advanced Settings", open=False):
452
+ with gr.Row():
453
+ with gr.Column():
454
+ ref_image = gr.Image(label="Input image (รูปต้นแบบ)", type="pil", height=240)
455
+ ip_scale = gr.Slider(0.0, 1.5, value=0.7, step=0.05,
456
+ label="Reference strength (IP-Adapter / FaceID)")
457
+ denoise = gr.Slider(0.1, 1.0, value=0.65, step=0.01,
458
+ label="Denoise strength (img2img · ต่ำ = อิงรูปมาก)")
459
+ with gr.Column():
460
+ negative_prompt = gr.Textbox(label="Negative prompt", lines=2)
461
+ with gr.Row():
462
+ steps = gr.Slider(1, 50, value=28, step=1, label="Steps")
463
+ guidance = gr.Slider(0.0, 15.0, value=6.5, step=0.1, label="Guidance (CFG)")
464
+ with gr.Row():
465
+ width = gr.Slider(384, 1280, value=512, step=64, label="Width")
466
+ height = gr.Slider(384, 1280, value=768, step=64, label="Height")
467
+ with gr.Row():
468
+ seed = gr.Number(value=-1, label="Seed (-1 = random)", precision=0)
469
+ randomize = gr.Checkbox(value=True, label="Randomize seed")
470
 
471
  # ---- wiring ----
472
  model_gallery.select(
 
492
  prompt.submit(generate, inputs=gen_inputs, outputs=[output, seed, status])
493
 
494
 
495
+
496
+ # ---- Tab 1 (identity) wiring ----
497
+ face_build_btn.click(
498
+ build_face_prompt,
499
+ inputs=[f_gender, f_age, f_ethnicity, f_faceshape, f_skin, f_eyecolor,
500
+ f_eyes, f_brow, f_nose, f_mouth, f_hair, f_expr],
501
+ outputs=face_prompt,
502
+ )
503
+ face_gen_btn.click(
504
+ generate_face,
505
+ inputs=[selected_id, face_prompt, translator],
506
+ outputs=[face_output, face_status],
507
+ )
508
+ send_face_btn.click(
509
+ send_face_to_scene,
510
+ inputs=[face_output],
511
+ outputs=[ref_image, mode_radio, send_note],
512
+ )
513
+
514
+
515
  if __name__ == "__main__":
516
  # allowed_paths lets Gradio serve the local model preview thumbnails.
517
  demo.queue(max_size=12).launch(allowed_paths=["previews"])