Files

372 lines
14 KiB
Python

from pathlib import Path
import os
import sys
import subprocess, json, tempfile
from huggingface_hub import snapshot_download
import huggingface_hub
import folder_paths
NODE_DIR = Path(__file__).parent.resolve()
REPO_NAME = "Make_It_Animatable"
REPO_DIR = NODE_DIR / REPO_NAME
REPO_URL = "https://github.com/jasongzy/Make-It-Animatable.git"
if NODE_DIR not in sys.path:
sys.path.insert(0, NODE_DIR)
def run_cmd(cmd):
print(f"[Make-It-Animatable] $ {' '.join(map(str, cmd))}")
process = subprocess.Popen(cmd, cwd=str(REPO_DIR), env=os.environ.copy(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
rc = process.poll()
if rc != 0:
raise subprocess.CalledProcessError(rc, cmd)
def clone_repo():
if REPO_DIR.exists():
print(f"[Make-It-Animatable] Repo already cloned at {REPO_DIR}")
return
print(f"[Make-It-Animatable] Cloning {REPO_URL} --recursive --single-branch...")
REPO_DIR.mkdir(parents=True, exist_ok=True)
run_cmd([
"git", "clone", "--recursive", "--single-branch",
REPO_URL, str(REPO_DIR)
])
def apply_patches():
import subprocess
patch_dir = NODE_DIR / "patches"
if not patch_dir.exists():
return
print("[Make-It-Animatable] Applying local patches...")
for patch_file in sorted(patch_dir.glob("*.patch")):
print(f" -> Applying {patch_file.name}")
result = subprocess.run(
["git", "apply", str(patch_file)],
cwd=REPO_DIR,
capture_output=True,
text=True,
)
if result.returncode != 0:
if "already exists" in result.stderr or "patch does not apply" in result.stderr:
print(f" Already applied or skipped: {patch_file.name}")
else:
print(result.stderr)
raise RuntimeError(f"Failed to apply patch {patch_file}")
else:
print(f" Success: {patch_file.name}")
def download_pretrained_models():
dir = REPO_DIR / "output" / "best" / "new"
dir.mkdir(parents=True, exist_ok=True)
if next(dir.iterdir(), None):
print("[Make-It-Animatable] pretrained models data already exists — skipping download")
return
print("[Make-It-Animatable] Downloading pretrained models via huggingface_hub...")
snapshot_download(
repo_type="model",
repo_id="jasongzy/Make-It-Animatable",
revision="eb12b71253361fd1a7216625a95144af3c58263e", # avoid breaking changes from new commits
local_dir=str(REPO_DIR),
allow_patterns=[
"output/best/new/*",
],
tqdm_class=huggingface_hub.utils.tqdm,
)
def download_mixamo_bones():
dir = REPO_DIR / "data" / "Mixamo"
dir.mkdir(parents=True, exist_ok=True)
if next(dir.iterdir(), None):
print("[Make-It-Animatable] Mixamo data already exists — skipping download")
return
print("[Make-It-Animatable] Downloading Mixamo dataset via huggingface_hub")
snapshot_download(
repo_type="dataset",
repo_id="jasongzy/Mixamo",
revision="b1c7f4975ea3261d3d0aa2379f6e24754ccde9d8", # avoid breaking changes from new commits
local_dir=str(dir),
allow_patterns=["bones*.fbx"],
tqdm_class=huggingface_hub.utils.tqdm,
)
print("[Make-It-Animatable] Mixamo data downloaded successfully!")
def find_uv_executable():
import shutil
import sysconfig
# 1. Try to find uv in PATH
uv_path = shutil.which("uv")
if uv_path:
return uv_path
# 2. Try importing 'uv' package and using find_uv_bin()
try:
import uv
uv_path = uv.find_uv_bin()
if uv_path and os.path.exists(uv_path):
return uv_path
except (ImportError, AttributeError, Exception):
pass
# 3. Check common sysconfig/sys paths
search_dirs = []
# sys.prefix (active venv)
search_dirs.append(os.path.join(sys.prefix, "Scripts") if sys.platform == "win32" else os.path.join(sys.prefix, "bin"))
search_dirs.append(sys.prefix)
# sys.base_prefix (base Python environment)
if hasattr(sys, "base_prefix") and sys.base_prefix:
search_dirs.append(os.path.join(sys.base_prefix, "Scripts") if sys.platform == "win32" else os.path.join(sys.base_prefix, "bin"))
search_dirs.append(sys.base_prefix)
# sysconfig paths
try:
search_dirs.append(sysconfig.get_path("scripts"))
except Exception:
pass
try:
search_dirs.append(sysconfig.get_path("scripts", vars={"base": getattr(sys, "base_prefix", sys.prefix)}))
except Exception:
pass
# Relative to sys.executable
exe_dir = os.path.dirname(sys.executable)
search_dirs.append(exe_dir)
search_dirs.append(os.path.join(exe_dir, "Scripts"))
search_dirs.append(os.path.dirname(exe_dir)) # One level up
# Check all search dirs for 'uv' or 'uv.exe'
binary_name = "uv.exe" if sys.platform == "win32" else "uv"
seen = set()
for d in search_dirs:
if not d:
continue
d_abs = os.path.abspath(d)
if d_abs in seen:
continue
seen.add(d_abs)
candidate = os.path.join(d_abs, binary_name)
if os.path.isfile(candidate):
return candidate
return None
def ensure_repo_venv():
req_file = REPO_DIR / "requirements.txt"
if not req_file.exists():
print("[Make-It-Animatable] No requirements.txt found")
return
venv_dir = REPO_DIR / "venv311"
marker_file = venv_dir / "install_completed.marker"
if venv_dir.exists() and marker_file.exists():
return
if sys.platform == "win32":
python = venv_dir / "Scripts" / "python.exe"
else:
python = venv_dir / "bin" / "python"
uv = find_uv_executable()
if not uv:
print("[Make-It-Animatable] uv binary not found. Attempting to install it...")
is_python_embedded = Path(sys.executable).parent.name == "python_embeded"
if is_python_embedded:
embedded_python = Path(sys.executable).parent / "python"
run_cmd([str(embedded_python), "-m", "pip", "install", "uv"])
else:
run_cmd([sys.executable, "-m", "pip", "install", "uv"])
# Try finding uv again after installation
uv = find_uv_executable()
if not uv:
raise FileNotFoundError("[Make-It-Animatable] Could not find or install the 'uv' binary.")
run_cmd([str(uv), "venv", "--clear", "--python", "3.11", str(venv_dir)])
print("[Make-It-Animatable] Installing Python dependencies from requirements.txt...")
run_cmd([str(uv), "pip", "install", "--python", str(python), "-r", str(req_file)])
# Touch the marker file to flag that the installation succeeded
marker_file.touch()
setup_complete = [False]
def setup_make_it_animatable():
try:
if setup_complete[0]:
return
clone_repo()
apply_patches()
download_pretrained_models()
download_mixamo_bones()
ensure_repo_venv()
setup_complete[0] = True
except Exception as e:
err_msg = str(e)
if "GatedRepoError" in type(e).__name__ or "401 Client Error" in err_msg or "gated repo" in err_msg.lower():
print("\n" + "="*80)
print("[Make-It-Animatable] ERROR: Failed to download gated Hugging Face files.")
print("Access to the jasongzy/Mixamo dataset is restricted (gated).")
print("To resolve this:")
print("1. Log in to Hugging Face or create an account if you don't have one.")
print("2. Visit https://huggingface.co/datasets/jasongzy/Mixamo and click 'Agree' or accept the terms to request access.")
print("3. Authenticate your local machine by either:")
print(" - Setting the HF_TOKEN environment variable in your terminal/system.")
print(" - Or running: huggingface-cli login")
print("="*80 + "\n")
print(f"[Make-It-Animatable] SETUP FAILED: {e}")
raise
def run_make_it_animatable(input_path: str, **node_kwargs):
NODE_DIR = Path(__file__).parent
script = NODE_DIR / "server.py"
REPO_DIR = NODE_DIR / "Make_It_Animatable"
python = REPO_DIR / "venv311" / "Scripts" / "python"
use_gs = node_kwargs.get("is_gs", False)
suffix = ".blend" if use_gs else ".glb"
rigged_suffix = f"_rigged{suffix}"
input_filename = Path(input_path).stem
output_filename = f"{input_filename}{rigged_suffix}"
output_dir = folder_paths.get_output_directory()
output_file = Path(output_dir) / output_filename
if output_file.exists():
output_file.unlink()
cmd = [
str(python), str(script),
"--input", str(input_path),
"--output", str(output_file),
"--kwargs", json.dumps(node_kwargs)
]
result = subprocess.run(cmd, capture_output=True, text=True)
if not output_file.exists():
print("Make-It-Animatable subprocess failed:")
print(result.stdout)
print(result.stderr)
raise RuntimeError(f"Make-It-Animatable failed. {result.stderr}")
return str(output_file)
class MakeItAnimatableRig:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_model_path": ("STRING", {"multiline": False}),
"no_fingers": ("BOOLEAN", {"default": True, "tooltip": "Whether the input model does not have ten separate fingers. Can also be used if the output has unsatisfactory finger results."}),
"use_normals": ("BOOLEAN", {"default": False, "tooltip": "Use normal information to improve performance when the input has limbs close to other ones."}),
"weight_postprocess": ("BOOLEAN", {"default": True, "tooltip": "Apply some empirical post-processes to the blend weights."}),
},
"optional": {
"animation_file": ("STRING", {"multiline": True, "default": "", "tooltip": "Path to Mixamo animation file to bake. Can be a single path or multiple paths separated by newlines or carriage returns."}),
}
}
RETURN_TYPES = (
"STRING",
)
RETURN_NAMES = (
"rigged_glb_path",
)
FUNCTION = "run"
CATEGORY = "Make-It-Animatable"
def run(
self,
input_model_path: str,
no_fingers: bool,
use_normals: bool,
weight_postprocess: bool,
animation_file: str = "",
):
setup_make_it_animatable()
input_path = input_model_path.strip()
if not input_path:
raise RuntimeError("input_model_path is empty.")
if not os.path.isfile(input_path):
raise RuntimeError(f"Input model not found: {input_path}")
result = run_make_it_animatable(input_path, is_gs=False, no_fingers=no_fingers, input_normal=use_normals, bw_fix=weight_postprocess, reset_to_rest=True, inplace=True, animation_file=animation_file if animation_file.strip() else None)
return (result,)
class MakeItAnimatableRigGS:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_model_path": ("STRING", {"multiline": False, "tooltip": "Must by Gaussian-Splat in `.ply` format."}),
"opacity_threshold": ("FLOAT", {"default": 0.01, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Only solid Gaussian Splats with opacities larger than this threshold are used in sampling."}),
"no_fingers": ("BOOLEAN", {"default": True, "tooltip": "Whether the input model does not have ten separate fingers. Can also be used if the output has unsatisfactory finger results."}),
"use_normals": ("BOOLEAN", {"default": False, "tooltip": "Use normal information to improve performance when the input has limbs close to other ones."}),
"weight_postprocess": ("BOOLEAN", {"default": True, "tooltip": "Apply some empirical post-processes to the blend weights."}),
},
"optional": {
"animation_file": ("STRING", {"multiline": True, "default": "", "tooltip": "Path to Mixamo animation file to bake. Can be a single path or multiple paths separated by newlines or carriage returns."}),
}
}
RETURN_TYPES = (
"STRING",
)
RETURN_NAMES = (
"rigged_blend_path",
)
FUNCTION = "run"
CATEGORY = "Make-It-Animatable"
def run(
self,
input_model_path: str,
opacity_threshold: str,
no_fingers: bool,
use_normals: bool,
weight_postprocess: bool,
animation_file: str = "",
):
setup_make_it_animatable()
input_path = input_model_path.strip()
if not input_path:
raise RuntimeError("input_model_path is empty.")
if not os.path.isfile(input_path):
raise RuntimeError(f"Input model not found: {input_path}")
result = run_make_it_animatable(input_path, is_gs=True, opacity_threshold=opacity_threshold, no_fingers=no_fingers, input_normal=use_normals, bw_fix=weight_postprocess, reset_to_rest=True, inplace=True, animation_file=animation_file if animation_file.strip() else None)
return (result,)
NODE_CLASS_MAPPINGS = {
"MakeItAnimatableRig": MakeItAnimatableRig,
"MakeItAnimatableRigGS": MakeItAnimatableRigGS,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MakeItAnimatableRig": "Make-It-Animatable (Auto-Rig Mesh)",
"MakeItAnimatableRigGS": "Make-It-Animatable Gaussian-Splat (Auto-Rig)",
}