Mesh_Rigger / app.py
jkorstad's picture
Update app.py
3561fbf unverified
raw
history blame
10.6 kB
import gradio as gr
import torch
import os
import sys
import tempfile
import shutil
import subprocess
# --- Configuration ---
# Path to the cloned UniRig repository directory within the Space
# IMPORTANT: You must clone the UniRig repository into this directory in your Hugging Face Space.
UNIRIG_REPO_DIR = os.path.join(os.path.dirname(__file__), "UniRig")
# Check if UniRig directory exists
if not os.path.isdir(UNIRIG_REPO_DIR):
# This message will appear in logs, Gradio app might fail to start fully.
print(f"ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
# Optionally, raise an error to make it more visible if the app starts
# raise RuntimeError(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
# Determine processing device (CUDA if available, otherwise CPU)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {DEVICE}")
if DEVICE.type == 'cuda':
print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
print(f"CUDA Version: {torch.version.cuda}")
# Note: UniRig scripts might manage device internally or via Hydra configs.
else:
print("Warning: CUDA not available or not detected by PyTorch. UniRig performance will be severely impacted.")
def run_unirig_command(command_args, step_name):
"""Helper function to run UniRig commands using subprocess."""
python_exe = sys.executable # Use the current python interpreter
cmd = [python_exe, "-m"] + command_args
print(f"Running {step_name}: {' '.join(cmd)}")
# UniRig scripts often expect to be run from the root of the UniRig repository
# because they use Hydra and relative paths for configs (e.g., conf/config.yaml)
process_env = os.environ.copy()
# Add UniRig's parent directory to PYTHONPATH so `import unirig` works if needed,
# and the UniRig directory itself so its internal imports work.
# However, `python -m` typically handles package discovery well if CWD is correct.
# process_env["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{process_env.get('PYTHONPATH', '')}"
try:
# Execute the command
# It's crucial to set `cwd=UNIRIG_REPO_DIR` for Hydra to find its configs.
result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
print(f"{step_name} output:\n{result.stdout}")
if result.stderr:
print(f"{step_name} errors (non-fatal if check=True passed):\n{result.stderr}")
except subprocess.CalledProcessError as e:
print(f"ERROR during {step_name}:")
print(f"Command: {' '.join(e.cmd)}")
print(f"Return code: {e.returncode}")
print(f"Stdout: {e.stdout}")
print(f"Stderr: {e.stderr}")
raise gr.Error(f"Error in UniRig {step_name}: {e.stderr[:500]}") # Show first 500 chars of error
except FileNotFoundError:
# This can happen if UNIRIG_REPO_DIR is not populated correctly or python_exe is wrong
print(f"ERROR: Could not find executable or script for {step_name}. Is UniRig cloned correctly in {UNIRIG_REPO_DIR}?")
raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs.")
# --- Core Rigging Function ---
def rig_glb_mesh_multistep(input_glb_file_obj):
"""
Takes an input GLB file object, rigs it using the new UniRig multi-step process,
and returns the path to the final rigged GLB file.
"""
if not os.path.isdir(UNIRIG_REPO_DIR):
raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Cannot proceed.")
if input_glb_file_obj is None:
raise gr.Error("No input file provided. Please upload a .glb mesh.")
input_glb_path = input_glb_file_obj.name # Path to the temporary uploaded file
# Create a dedicated temporary directory for all intermediate and final files for this run
# This helps in organization and cleanup.
processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
print(f"Using temporary processing directory: {processing_temp_dir}")
try:
# Define paths for intermediate and final files within the processing_temp_dir
# UniRig scripts expect output paths.
base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
# Step 1: Skeleton Prediction
# Output is typically an FBX file for the skeleton
temp_skeleton_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
print("Step 1: Predicting Skeleton...")
# Command: python -m unirig.predict_skeleton +input_path=<input_glb_path> +output_path=<temp_skeleton_path>
# Note: UniRig's scripts might have default output locations or require specific Hydra overrides.
# The `+` syntax is for Hydra overrides.
# Check UniRig's `conf/predict_skeleton.yaml` for default config values.
run_unirig_command([
"unirig.predict_skeleton",
f"input.path={input_glb_path}", # Use dot notation for Hydra parameters
f"output.path={temp_skeleton_path}",
# Add other necessary overrides, e.g., for device if not auto-detected well
# f"device={str(DEVICE)}" # If UniRig's script accepts this override
], "Skeleton Prediction")
print(f"Skeleton predicted at: {temp_skeleton_path}")
if not os.path.exists(temp_skeleton_path):
raise gr.Error("Skeleton prediction failed to produce an output file.")
# Step 2: Skinning Weight Prediction
# Input: skeleton FBX and original GLB. Output: skinned FBX (or other format)
temp_skin_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
print("Step 2: Predicting Skinning Weights...")
# Command: python -m unirig.predict_skin +input_path=<temp_skeleton_path> +output_path=<temp_skin_path> +source_mesh_path=<input_glb_path>
run_unirig_command([
"unirig.predict_skin",
f"input.skeleton_path={temp_skeleton_path}", # Check exact Hydra param name in UniRig
f"input.source_mesh_path={input_glb_path}", # Check exact Hydra param name
f"output.path={temp_skin_path}",
], "Skinning Prediction")
print(f"Skinning predicted at: {temp_skin_path}")
if not os.path.exists(temp_skin_path):
raise gr.Error("Skinning prediction failed to produce an output file.")
# Step 3: Merge Skeleton/Skin with Original Mesh
# Input: original GLB and the skin FBX (which contains skeleton + weights). Output: final rigged GLB
final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
print("Step 3: Merging Results...")
# Command: python -m unirig.merge_skeleton_skin +source_path=<temp_skin_path> +target_path=<input_glb_path> +output_path=<final_rigged_glb_path>
run_unirig_command([
"unirig.merge_skeleton_skin",
f"input.source_rig_path={temp_skin_path}", # Path to the file with skeleton and skin weights
f"input.target_mesh_path={input_glb_path}", # Path to the original mesh
f"output.path={final_rigged_glb_path}",
], "Merging")
print(f"Final rigged mesh at: {final_rigged_glb_path}")
if not os.path.exists(final_rigged_glb_path):
raise gr.Error("Merging process failed to produce the final rigged GLB file.")
# The final_rigged_glb_path needs to be accessible by Gradio to serve it.
# Gradio usually copies temp files it creates, but here we created it.
# We return the path, and Gradio should handle it.
# The processing_temp_dir will be cleaned up by Gradio if input_glb_file_obj is from gr.File
# or we can clean it up if we copy the final file to a Gradio managed temp location.
# For gr.Model3D, returning a path is fine.
return final_rigged_glb_path
except gr.Error: # Re-raise Gradio errors directly
raise
except Exception as e:
print(f"An unexpected error occurred: {e}")
# Clean up the processing directory in case of an unhandled error
if os.path.exists(processing_temp_dir):
shutil.rmtree(processing_temp_dir)
raise gr.Error(f"An unexpected error occurred during processing: {str(e)}")
# Note: Do not clean up processing_temp_dir in a `finally` block here if returning path from it,
# as Gradio needs the file to exist to serve it. Gradio's gr.File output type handles temp file cleanup.
# If outputting gr.File, copy the final file to a new tempfile managed by Gradio.
# For gr.Model3D, path is fine.
# --- Gradio Interface ---
theme = gr.themes.Soft(
primary_hue=gr.themes.colors.sky,
secondary_hue=gr.themes.colors.blue,
neutral_hue=gr.themes.colors.slate,
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
)
iface = gr.Interface(
fn=rig_glb_mesh_multistep,
inputs=gr.File(label="Upload .glb Mesh File", type="file"),
outputs=gr.Model3D(
label="Rigged 3D Model (.glb)",
clear_color=[0.8, 0.8, 0.8, 1.0],
# Note: Model3D might have issues with complex GLBs or certain rigging structures.
# A gr.File output for download might be a safer fallback.
# outputs=[gr.Model3D(...), gr.File(label="Download Rigged GLB")]
),
title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
description=(
"Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
"The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
"This may take several minutes. Ensure your GLB has clean geometry.\n"
f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{UNIRIG_REPO_DIR}'.\n"
f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
),
cache_examples=False,
theme=theme,
allow_flagging="never"
)
if __name__ == "__main__":
# Perform a quick check for UniRig directory on launch
if not os.path.isdir(UNIRIG_REPO_DIR):
print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will likely fail.")
# You could display this error in the Gradio interface itself using a dummy function or Markdown.
# For local testing, you might need to set PYTHONPATH or ensure UniRig is installed.
# Example: os.environ["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{os.environ.get('PYTHONPATH', '')}"
iface.launch()