"""LmGirl T-pose: Hunyuan 50k, then Studio low-poly, Studio UV, bake, and 5000-face reduction. No skeleton. Step 05 runs only when the baked mesh is more than 30% over 5000 triangles. """ from __future__ import annotations import json import shutil import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] STUDIO = Path(r"D:\UD\AI\XModel\LLM3DGraphics") BLENDER = Path(r"D:\Tools\Blender\blender-5.0.0-windows-x64\blender.exe") PY = Path(r"D:\UD\AI\XModel\LLM3DGraphics\.venv\Scripts\python.exe") IMAGE = ROOT / "Doc/Design/Character/LmGirl/lm_girl_tpose.jpg" HIGH = ROOT / "Tools/output/lmgirl_tpose_3d/lmgirl.glb" OUT = ROOT / "Tools/output/lmgirl_tpose_studio" NAME = "LmGirlTpose" TARGET = 5000 SIZE = 1024 sys.path.insert(0, str(ROOT / "Tools")) from hunyuan_3d import auth # noqa: E402 from hunyuan_3d.glb_check import assert_glb_textures_ok # noqa: E402 from model_tools.bl_decimate import FBX_YUP_EXPORT_VERSION # noqa: E402 sys.path.insert(0, str(STUDIO)) from backend import hunyuan_studio as studio # noqa: E402 from backend.retopology import _run_blender_step, compress_bake_maps, limit_studio_faces # noqa: E402 def update(**values): print(json.dumps(values, ensure_ascii=False), flush=True) def generate_high() -> None: if HIGH.is_file() and HIGH.stat().st_size > 1_000_000: update(stage="reuse high mesh", path=str(HIGH)) return HIGH.parent.mkdir(parents=True, exist_ok=True) update(stage="hunyuan 50k", image=str(IMAGE)) subprocess.run( [str(PY), str(ROOT / "Tools/hunyuan_3d_generate.py"), "--image", str(IMAGE), "--output", str(HIGH), "--faces", "50k", "--no-interactive-login", "--result-only"], check=True, timeout=1500, ) assert_glb_textures_ok(HIGH) def main() -> int: if not IMAGE.is_file(): raise SystemExit(f"missing design image: {IMAGE}") generate_high() assert_glb_textures_ok(HIGH) OUT.mkdir(parents=True, exist_ok=True) shutil.copy2(HIGH, OUT / "model.glb") studio.is_logged_in = lambda client_id=None: auth.is_logged_in() studio.load_storage_state = lambda client_id=None: ( json.loads(auth.storage_state_path().read_text(encoding="utf-8")), None, ) uv = OUT / "model_lowpoly_uv.glb" if uv.is_file(): studio.validate_uv_glb(uv) update(stage="reuse studio uv", path=str(uv)) else: meta_path = OUT / "hunyuan_studio.json" meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.is_file() else {} studio.process_existing( HIGH, OUT, update, target_faces=TARGET, resume_low_id=meta.get("lowpoly_works_id"), resume_uv_id=meta.get("uv_works_id"), ) aligned = OUT / "model_lowpoly_uv_aligned.glb" if not aligned.is_file(): with (OUT / "alignment.log").open("w", encoding="utf-8") as log: subprocess.run( [str(BLENDER), "-b", "--factory-startup", "--python", str(ROOT / "Tools/model_tools/bl_align_studio_uv.py"), "--", str(OUT)], stdout=log, stderr=subprocess.STDOUT, check=True, timeout=180, ) baked = OUT / "model_baked.glb" if not baked.is_file(): _run_blender_step( OUT, str(BLENDER), "blender_lowpoly_bake.py", (OUT / "model.glb", baked, 1, SIZE, aligned), "bake.log", marker="Lowpoly bake saved to", ) compress_bake_maps(OUT) shutil.copy2(baked, OUT / "model_ex.glb") mesh, decimation = limit_studio_faces(OUT, TARGET, str(BLENDER), update) static = OUT / "static" / f"{NAME}.fbx" export_receipt = OUT / "static_export.json" export_info = json.loads(export_receipt.read_text(encoding="utf-8")) if export_receipt.is_file() else {} if not static.is_file() or export_info.get("fbxAxes", {}).get("exportVersion") != FBX_YUP_EXPORT_VERSION: with (OUT / "static_export.log").open("w", encoding="utf-8") as log: subprocess.run( [str(BLENDER), "-b", "-t", "1", "--factory-startup", "--python", str(ROOT / "Tools/model_tools/bl_prepare_studio_trial.py"), "--", str(OUT), NAME], stdout=log, stderr=subprocess.STDOUT, check=True, timeout=180, ) receipt = { "ok": True, "character": NAME, "image": "Doc/Design/Character/LmGirl/lm_girl_tpose.jpg", "source": "Tools/output/lmgirl_tpose_3d/lmgirl.glb", "targetTriangles": TARGET, "textureSize": SIZE, "triangles": len(mesh.faces), "vertices": len(mesh.vertices), "decimation": decimation, "final": "Tools/output/lmgirl_tpose_studio/model_ex.glb", "fbx": f"Tools/output/lmgirl_tpose_studio/static/{NAME}.fbx", "fbxAxes": json.loads(export_receipt.read_text(encoding="utf-8")).get("fbxAxes"), "rig": False, } (OUT / "mesh_receipt.json").write_text(json.dumps(receipt, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(receipt, ensure_ascii=False, indent=2), flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())