Initial commit: AIGame Unity client with URP, HybridCLR, YooAsset and tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 14:21:07 +08:00
co-authored by Cursor
commit f3f66ab063
931 changed files with 112366 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""腾讯混元3D 图生3D 工具。
支持:
- AI / Agent CLI 调用(参数或 JSON)
- 流程回调(HTTP webhook / 状态文件 / NDJSON 事件流)
- 未登录时弹出 Edge 微信扫码,取得凭证后继续生成
依赖:playwright + 本机 Microsoft Edge。见 Tools/requirements-hunyuan.txt
"""
from __future__ import annotations
import argparse
import json
import shutil
import sys
import uuid
from pathlib import Path
from typing import Any, Dict, Optional, Sequence
# 允许直接 `python Tools/hunyuan_3d_generate.py` 运行
_TOOLS_DIR = Path(__file__).resolve().parent
if str(_TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(_TOOLS_DIR))
from hunyuan_3d.auth import ( # noqa: E402
ensure_login,
is_logged_in,
jobs_dir,
logout,
start_wechat_login_blocking,
status,
)
from hunyuan_3d.client import FACE_LABEL, FACE_LABELS, generate_from_image # noqa: E402
from hunyuan_3d.events import CallbackHub # noqa: E402
class GenerateError(RuntimeError):
"""生成失败。"""
def _load_json_arg(raw: Optional[str]) -> Dict[str, Any]:
if not raw:
return {}
if raw == "-":
return json.load(sys.stdin)
path = Path(raw)
if path.is_file():
return json.loads(path.read_text(encoding="utf-8"))
return json.loads(raw)
def _normalize_faces(value: Optional[str]) -> str:
text = (value or FACE_LABEL).strip().lower()
aliases = {
"50k": "50k",
"50": "50k",
"50000": "50k",
"500k": "500k",
"500000": "500k",
"1m": "1m",
"1000000": "1m",
"1": "1m",
}
label = aliases.get(text, text)
if label not in FACE_LABELS:
raise GenerateError(f"面数须为 {list(FACE_LABELS)} 之一,收到: {value}")
return label
def _make_update(callbacks: CallbackHub):
def update(**payload: Any) -> None:
event = payload.pop("event", "stage")
callbacks.emit(event, **payload)
return update
def run_generate(
*,
image: str,
output: str,
faces: str = FACE_LABEL,
interactive_login: bool = True,
login_timeout: int = 180,
callback_url: Optional[str] = None,
callback_file: Optional[str] = None,
quiet: bool = False,
) -> Dict[str, Any]:
callbacks = CallbackHub(
callback_url=callback_url,
callback_file=callback_file,
quiet=quiet,
)
update = _make_update(callbacks)
image_path = Path(image).expanduser().resolve()
if not image_path.is_file():
raise GenerateError(f"参考图不存在: {image_path}")
output_path = Path(output).expanduser().resolve()
if output_path.suffix.lower() != ".glb":
if output_path.is_dir() or not output_path.suffix:
output_path = output_path / f"{image_path.stem}.glb"
else:
raise GenerateError("输出路径须为 .glb 文件或目录")
output_path.parent.mkdir(parents=True, exist_ok=True)
face_label = _normalize_faces(faces)
job_id = uuid.uuid4().hex
work_dir = jobs_dir() / job_id
work_dir.mkdir(parents=True, exist_ok=True)
update(
event="started",
stage="开始混元图生3D",
progress=1,
job_id=job_id,
image=str(image_path),
output=str(output_path),
faces=face_label,
)
try:
ensure_login(
interactive=interactive_login,
timeout_sec=login_timeout,
update=update,
)
attempts = 0
while True:
attempts += 1
try:
mesh = generate_from_image(
image_path, work_dir, update, face_label=face_label
)
break
except PermissionError as exc:
if not interactive_login or attempts >= 2:
raise GenerateError(str(exc)) from exc
update(
event="login_required",
stage="凭证失效,重新微信登录",
progress=3,
error=str(exc),
)
# 清掉失效会话再扫码
if is_logged_in():
logout()
start_wechat_login_blocking(
timeout_sec=login_timeout, update=update
)
shutil.copy2(mesh, output_path)
result = {
"ok": True,
"job_id": job_id,
"status": "succeeded",
"progress": 100,
"faces": face_label,
"image": str(image_path),
"output": str(output_path),
"work_dir": str(work_dir),
"bytes": output_path.stat().st_size,
"hint": "生成完成后请在 Doc/ResDesc_Model.md 登记该资源",
}
callbacks.emit("succeeded", **result)
return result
except GenerateError:
raise
except Exception as exc:
err = {"ok": False, "error": str(exc), "job_id": job_id, "work_dir": str(work_dir)}
callbacks.emit("failed", **err)
raise GenerateError(str(exc)) from exc
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="hunyuan_3d_generate",
description="腾讯混元3D 图生3D(微信登录 + Playwright + GLB 下载)",
)
p.add_argument("--image", "-i", help="参考图本地路径")
p.add_argument("--output", "-o", help="输出 GLB 路径(文件或目录)")
p.add_argument(
"--faces",
default=FACE_LABEL,
help=f"原始面数:{'/'.join(FACE_LABELS)},默认 {FACE_LABEL}",
)
p.add_argument(
"--login",
action="store_true",
help="仅弹出微信扫码登录并保存凭证",
)
p.add_argument("--logout", action="store_true", help="清除本地混元登录凭证")
p.add_argument("--status", action="store_true", help="打印当前登录状态 JSON")
p.add_argument(
"--no-interactive-login",
action="store_true",
help="未登录时不弹窗,直接失败(适合 CI)",
)
p.add_argument(
"--login-timeout",
type=int,
default=180,
help="微信扫码等待秒数,默认 180",
)
p.add_argument("--callback-url", default=None, help="流程回调 URL:每个事件 POST JSON")
p.add_argument(
"--callback-file",
default=None,
help="流程回调状态文件(持续覆盖写入 latest/history)",
)
p.add_argument(
"--json",
dest="json_input",
default=None,
help='JSON 参数(对象字符串 / 文件路径 / "-" 读 stdin)',
)
p.add_argument(
"--result-only",
action="store_true",
help="只输出最终结果 JSON(抑制中间 NDJSON 事件到 stdout,仍写回调)",
)
p.add_argument(
"--quiet",
action="store_true",
help="不向 stdout 打印事件流(最终结果仍打印)",
)
return p
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
payload = _load_json_arg(args.json_input)
quiet_events = bool(args.quiet or args.result_only or payload.get("quiet"))
if args.logout or payload.get("logout"):
print(json.dumps(logout(), ensure_ascii=False), flush=True)
return 0
if args.status or payload.get("status"):
print(json.dumps(status(), ensure_ascii=False), flush=True)
return 0
if args.login or payload.get("login"):
callbacks = CallbackHub(
callback_url=payload.get("callback_url") or args.callback_url,
callback_file=payload.get("callback_file") or args.callback_file,
quiet=quiet_events,
)
update = _make_update(callbacks)
try:
result = start_wechat_login_blocking(
timeout_sec=int(payload.get("login_timeout") or args.login_timeout),
update=update,
)
except Exception as exc: # noqa: BLE001
err = {"ok": False, "error": str(exc)}
print(json.dumps(err, ensure_ascii=False), flush=True)
return 1
print(json.dumps({"ok": True, **result}, ensure_ascii=False), flush=True)
return 0
image = payload.get("image") or args.image
output = payload.get("output") or args.output
if not image or not output:
parser.error("生成模式必须提供 --image/-i 与 --output/-o(或 JSON 同名字段)")
try:
result = run_generate(
image=image,
output=output,
faces=payload.get("faces") or args.faces,
interactive_login=not (
args.no_interactive_login or payload.get("no_interactive_login")
),
login_timeout=int(payload.get("login_timeout") or args.login_timeout),
callback_url=payload.get("callback_url") or args.callback_url,
callback_file=payload.get("callback_file") or args.callback_file,
quiet=quiet_events,
)
except GenerateError as exc:
err = {"ok": False, "error": str(exc)}
print(json.dumps(err, ensure_ascii=False), flush=True)
return 1
except Exception as exc: # noqa: BLE001
err = {"ok": False, "error": str(exc)}
print(json.dumps(err, ensure_ascii=False), flush=True)
return 2
print(json.dumps(result, ensure_ascii=False), flush=True)
return 0
if __name__ == "__main__":
sys.exit(main())