Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -1,172 +1,202 @@
|
|
1 |
import gradio as gr
|
2 |
import torch
|
3 |
-
import trimesh
|
4 |
import os
|
5 |
import sys
|
6 |
import tempfile
|
7 |
import shutil
|
8 |
-
|
9 |
-
# Add UniRig source directory to Python path
|
10 |
-
# Assuming UniRig files are in a subdirectory named 'UniRig_src'
|
11 |
-
sys.path.append(os.path.join(os.path.dirname(__file__), 'UniRig_src'))
|
12 |
-
|
13 |
-
# Conditional import for AutoRigger and setup_source_mesh
|
14 |
-
# This helps in providing a clearer error if UniRig_src is not found
|
15 |
-
try:
|
16 |
-
from autorig import AutoRigger
|
17 |
-
from utils import setup_source_mesh
|
18 |
-
except ImportError as e:
|
19 |
-
print("Error importing from UniRig_src. Make sure the UniRig source files are in the 'UniRig_src' directory.")
|
20 |
-
print(f"Details: {e}")
|
21 |
-
# Define dummy functions if import fails, so Gradio can still load with an error message
|
22 |
-
def AutoRigger(*args, **kwargs):
|
23 |
-
raise RuntimeError("UniRig AutoRigger could not be loaded. Check UniRig_src setup.")
|
24 |
-
def setup_source_mesh(mesh, *args, **kwargs):
|
25 |
-
raise RuntimeError("UniRig setup_source_mesh could not be loaded. Check UniRig_src setup.")
|
26 |
|
27 |
# --- Configuration ---
|
28 |
-
#
|
29 |
-
#
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
-
# Check if model files exist
|
35 |
-
if not os.path.exists(SMPL_SKELETON_PATH) or not os.path.exists(SKIN_KPS_PREDICTOR_PATH):
|
36 |
-
print(f"Warning: Model files not found at {MODEL_DIR}. Please ensure smpl_skeleton.pkl and skin_kps_predictor.pkl are present.")
|
37 |
|
38 |
# Determine processing device (CUDA if available, otherwise CPU)
|
39 |
-
# ZeroGPU on Hugging Face Spaces should provide CUDA
|
40 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
41 |
print(f"Using device: {DEVICE}")
|
42 |
if DEVICE.type == 'cuda':
|
43 |
print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
|
44 |
print(f"CUDA Version: {torch.version.cuda}")
|
|
|
45 |
else:
|
46 |
-
print("CUDA not available
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
48 |
|
49 |
# --- Core Rigging Function ---
|
50 |
-
def
|
51 |
"""
|
52 |
-
Takes an input GLB file, rigs it using
|
|
|
53 |
"""
|
54 |
-
if
|
|
|
|
|
|
|
55 |
raise gr.Error("No input file provided. Please upload a .glb mesh.")
|
56 |
|
57 |
-
input_glb_path =
|
58 |
|
59 |
-
#
|
60 |
-
|
61 |
-
|
|
|
62 |
|
63 |
try:
|
64 |
-
#
|
65 |
-
|
66 |
-
|
67 |
-
output_glb_path = os.path.join(temp_dir, output_glb_filename)
|
68 |
-
|
69 |
-
# 1. Load the mesh using trimesh
|
70 |
-
print(f"Loading mesh from: {input_glb_path}")
|
71 |
-
mesh = trimesh.load_mesh(input_glb_path, force='mesh', process=False)
|
72 |
-
|
73 |
-
if not isinstance(mesh, trimesh.Trimesh):
|
74 |
-
# If it's a Scene object, try to get a single geometry
|
75 |
-
if isinstance(mesh, trimesh.Scene):
|
76 |
-
if len(mesh.geometry) == 0:
|
77 |
-
raise gr.Error("Input GLB file contains no mesh geometry.")
|
78 |
-
# Concatenate all meshes in the scene into a single mesh
|
79 |
-
# This is a common approach, but might not be ideal for all GLB files
|
80 |
-
print(f"Input is a scene with {len(mesh.geometry)} geometries. Attempting to merge.")
|
81 |
-
mesh = trimesh.util.concatenate(list(mesh.geometry.values()))
|
82 |
-
if not isinstance(mesh, trimesh.Trimesh):
|
83 |
-
raise gr.Error(f"Could not extract a valid mesh from the GLB scene. Found type: {type(mesh)}")
|
84 |
-
else:
|
85 |
-
raise gr.Error(f"Failed to load a valid mesh from the input file. Loaded type: {type(mesh)}")
|
86 |
-
|
87 |
-
print("Mesh loaded successfully.")
|
88 |
-
|
89 |
-
# 2. Preprocess the mesh (as per UniRig's example)
|
90 |
-
# This step is crucial for UniRig to work correctly.
|
91 |
-
# It involves canonicalization and remeshing.
|
92 |
-
print("Preprocessing mesh...")
|
93 |
-
mesh = setup_source_mesh(mesh, device=DEVICE)
|
94 |
-
print("Mesh preprocessing complete.")
|
95 |
-
|
96 |
-
# 3. Initialize the AutoRigger
|
97 |
-
# Ensure model files are accessible
|
98 |
-
if not os.path.exists(SMPL_SKELETON_PATH) or not os.path.exists(SKIN_KPS_PREDICTOR_PATH):
|
99 |
-
raise gr.Error(f"UniRig model files not found. Searched in {MODEL_DIR}. Please check your Space's file structure.")
|
100 |
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
#
|
106 |
-
|
107 |
-
# The
|
108 |
-
#
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
except Exception as e:
|
124 |
-
print(f"
|
125 |
-
# Clean up
|
126 |
-
if
|
127 |
-
shutil.rmtree(
|
128 |
-
|
129 |
-
|
130 |
-
#
|
131 |
-
#
|
132 |
-
#
|
133 |
|
134 |
# --- Gradio Interface ---
|
135 |
-
# Define a custom theme (Blue and Charcoal Gray)
|
136 |
-
# Using Soft theme with sky blue and slate gray
|
137 |
theme = gr.themes.Soft(
|
138 |
-
primary_hue=gr.themes.colors.sky,
|
139 |
-
secondary_hue=gr.themes.colors.blue,
|
140 |
-
neutral_hue=gr.themes.colors.slate,
|
141 |
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
|
142 |
-
).set(
|
143 |
-
# Further fine-tuning if needed
|
144 |
-
# button_primary_background_fill="*primary_500",
|
145 |
-
# button_primary_text_color="white",
|
146 |
)
|
147 |
|
148 |
-
# Interface definition
|
149 |
iface = gr.Interface(
|
150 |
-
fn=
|
151 |
-
inputs=gr.File(label="Upload .glb Mesh File", type="file"),
|
152 |
-
outputs=gr.Model3D(
|
153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
description=(
|
155 |
-
"Upload a 3D mesh in `.glb` format. This application uses UniRig to automatically rig the mesh.\n"
|
156 |
-
"The process
|
157 |
-
|
|
|
158 |
f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
|
159 |
),
|
160 |
-
|
161 |
-
# Add paths to example GLB files if you include them in your Space
|
162 |
-
# e.g., [os.path.join(os.path.dirname(__file__), "examples/sample_mesh.glb")]
|
163 |
-
],
|
164 |
-
cache_examples=False, # Set to True if you have static examples and want to pre-process them
|
165 |
theme=theme,
|
166 |
allow_flagging="never"
|
167 |
)
|
168 |
|
169 |
if __name__ == "__main__":
|
170 |
-
|
171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
iface.launch()
|
|
|
1 |
import gradio as gr
|
2 |
import torch
|
|
|
3 |
import os
|
4 |
import sys
|
5 |
import tempfile
|
6 |
import shutil
|
7 |
+
import subprocess
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
# --- Configuration ---
|
10 |
+
# Path to the cloned UniRig repository directory within the Space
|
11 |
+
# IMPORTANT: You must clone the UniRig repository into this directory in your Hugging Face Space.
|
12 |
+
UNIRIG_REPO_DIR = os.path.join(os.path.dirname(__file__), "UniRig")
|
13 |
+
|
14 |
+
# Check if UniRig directory exists
|
15 |
+
if not os.path.isdir(UNIRIG_REPO_DIR):
|
16 |
+
# This message will appear in logs, Gradio app might fail to start fully.
|
17 |
+
print(f"ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
|
18 |
+
# Optionally, raise an error to make it more visible if the app starts
|
19 |
+
# raise RuntimeError(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
|
20 |
|
|
|
|
|
|
|
21 |
|
22 |
# Determine processing device (CUDA if available, otherwise CPU)
|
|
|
23 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
24 |
print(f"Using device: {DEVICE}")
|
25 |
if DEVICE.type == 'cuda':
|
26 |
print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
|
27 |
print(f"CUDA Version: {torch.version.cuda}")
|
28 |
+
# Note: UniRig scripts might manage device internally or via Hydra configs.
|
29 |
else:
|
30 |
+
print("Warning: CUDA not available or not detected by PyTorch. UniRig performance will be severely impacted.")
|
31 |
+
|
32 |
+
def run_unirig_command(command_args, step_name):
|
33 |
+
"""Helper function to run UniRig commands using subprocess."""
|
34 |
+
python_exe = sys.executable # Use the current python interpreter
|
35 |
+
cmd = [python_exe, "-m"] + command_args
|
36 |
+
|
37 |
+
print(f"Running {step_name}: {' '.join(cmd)}")
|
38 |
+
|
39 |
+
# UniRig scripts often expect to be run from the root of the UniRig repository
|
40 |
+
# because they use Hydra and relative paths for configs (e.g., conf/config.yaml)
|
41 |
+
process_env = os.environ.copy()
|
42 |
+
|
43 |
+
# Add UniRig's parent directory to PYTHONPATH so `import unirig` works if needed,
|
44 |
+
# and the UniRig directory itself so its internal imports work.
|
45 |
+
# However, `python -m` typically handles package discovery well if CWD is correct.
|
46 |
+
# process_env["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{process_env.get('PYTHONPATH', '')}"
|
47 |
+
|
48 |
+
try:
|
49 |
+
# Execute the command
|
50 |
+
# It's crucial to set `cwd=UNIRIG_REPO_DIR` for Hydra to find its configs.
|
51 |
+
result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
|
52 |
+
print(f"{step_name} output:\n{result.stdout}")
|
53 |
+
if result.stderr:
|
54 |
+
print(f"{step_name} errors (non-fatal if check=True passed):\n{result.stderr}")
|
55 |
+
except subprocess.CalledProcessError as e:
|
56 |
+
print(f"ERROR during {step_name}:")
|
57 |
+
print(f"Command: {' '.join(e.cmd)}")
|
58 |
+
print(f"Return code: {e.returncode}")
|
59 |
+
print(f"Stdout: {e.stdout}")
|
60 |
+
print(f"Stderr: {e.stderr}")
|
61 |
+
raise gr.Error(f"Error in UniRig {step_name}: {e.stderr[:500]}") # Show first 500 chars of error
|
62 |
+
except FileNotFoundError:
|
63 |
+
# This can happen if UNIRIG_REPO_DIR is not populated correctly or python_exe is wrong
|
64 |
+
print(f"ERROR: Could not find executable or script for {step_name}. Is UniRig cloned correctly in {UNIRIG_REPO_DIR}?")
|
65 |
+
raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs.")
|
66 |
|
67 |
|
68 |
# --- Core Rigging Function ---
|
69 |
+
def rig_glb_mesh_multistep(input_glb_file_obj):
|
70 |
"""
|
71 |
+
Takes an input GLB file object, rigs it using the new UniRig multi-step process,
|
72 |
+
and returns the path to the final rigged GLB file.
|
73 |
"""
|
74 |
+
if not os.path.isdir(UNIRIG_REPO_DIR):
|
75 |
+
raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Cannot proceed.")
|
76 |
+
|
77 |
+
if input_glb_file_obj is None:
|
78 |
raise gr.Error("No input file provided. Please upload a .glb mesh.")
|
79 |
|
80 |
+
input_glb_path = input_glb_file_obj.name # Path to the temporary uploaded file
|
81 |
|
82 |
+
# Create a dedicated temporary directory for all intermediate and final files for this run
|
83 |
+
# This helps in organization and cleanup.
|
84 |
+
processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
|
85 |
+
print(f"Using temporary processing directory: {processing_temp_dir}")
|
86 |
|
87 |
try:
|
88 |
+
# Define paths for intermediate and final files within the processing_temp_dir
|
89 |
+
# UniRig scripts expect output paths.
|
90 |
+
base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
+
# Step 1: Skeleton Prediction
|
93 |
+
# Output is typically an FBX file for the skeleton
|
94 |
+
temp_skeleton_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
|
95 |
+
print("Step 1: Predicting Skeleton...")
|
96 |
+
# Command: python -m unirig.predict_skeleton +input_path=<input_glb_path> +output_path=<temp_skeleton_path>
|
97 |
+
# Note: UniRig's scripts might have default output locations or require specific Hydra overrides.
|
98 |
+
# The `+` syntax is for Hydra overrides.
|
99 |
+
# Check UniRig's `conf/predict_skeleton.yaml` for default config values.
|
100 |
+
run_unirig_command([
|
101 |
+
"unirig.predict_skeleton",
|
102 |
+
f"input.path={input_glb_path}", # Use dot notation for Hydra parameters
|
103 |
+
f"output.path={temp_skeleton_path}",
|
104 |
+
# Add other necessary overrides, e.g., for device if not auto-detected well
|
105 |
+
# f"device={str(DEVICE)}" # If UniRig's script accepts this override
|
106 |
+
], "Skeleton Prediction")
|
107 |
+
print(f"Skeleton predicted at: {temp_skeleton_path}")
|
108 |
+
if not os.path.exists(temp_skeleton_path):
|
109 |
+
raise gr.Error("Skeleton prediction failed to produce an output file.")
|
110 |
+
|
111 |
+
# Step 2: Skinning Weight Prediction
|
112 |
+
# Input: skeleton FBX and original GLB. Output: skinned FBX (or other format)
|
113 |
+
temp_skin_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
|
114 |
+
print("Step 2: Predicting Skinning Weights...")
|
115 |
+
# Command: python -m unirig.predict_skin +input_path=<temp_skeleton_path> +output_path=<temp_skin_path> +source_mesh_path=<input_glb_path>
|
116 |
+
run_unirig_command([
|
117 |
+
"unirig.predict_skin",
|
118 |
+
f"input.skeleton_path={temp_skeleton_path}", # Check exact Hydra param name in UniRig
|
119 |
+
f"input.source_mesh_path={input_glb_path}", # Check exact Hydra param name
|
120 |
+
f"output.path={temp_skin_path}",
|
121 |
+
], "Skinning Prediction")
|
122 |
+
print(f"Skinning predicted at: {temp_skin_path}")
|
123 |
+
if not os.path.exists(temp_skin_path):
|
124 |
+
raise gr.Error("Skinning prediction failed to produce an output file.")
|
125 |
+
|
126 |
+
# Step 3: Merge Skeleton/Skin with Original Mesh
|
127 |
+
# Input: original GLB and the skin FBX (which contains skeleton + weights). Output: final rigged GLB
|
128 |
+
final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
|
129 |
+
print("Step 3: Merging Results...")
|
130 |
+
# Command: python -m unirig.merge_skeleton_skin +source_path=<temp_skin_path> +target_path=<input_glb_path> +output_path=<final_rigged_glb_path>
|
131 |
+
run_unirig_command([
|
132 |
+
"unirig.merge_skeleton_skin",
|
133 |
+
f"input.source_rig_path={temp_skin_path}", # Path to the file with skeleton and skin weights
|
134 |
+
f"input.target_mesh_path={input_glb_path}", # Path to the original mesh
|
135 |
+
f"output.path={final_rigged_glb_path}",
|
136 |
+
], "Merging")
|
137 |
+
print(f"Final rigged mesh at: {final_rigged_glb_path}")
|
138 |
+
if not os.path.exists(final_rigged_glb_path):
|
139 |
+
raise gr.Error("Merging process failed to produce the final rigged GLB file.")
|
140 |
+
|
141 |
+
# The final_rigged_glb_path needs to be accessible by Gradio to serve it.
|
142 |
+
# Gradio usually copies temp files it creates, but here we created it.
|
143 |
+
# We return the path, and Gradio should handle it.
|
144 |
+
# The processing_temp_dir will be cleaned up by Gradio if input_glb_file_obj is from gr.File
|
145 |
+
# or we can clean it up if we copy the final file to a Gradio managed temp location.
|
146 |
+
# For gr.Model3D, returning a path is fine.
|
147 |
+
return final_rigged_glb_path
|
148 |
+
|
149 |
+
except gr.Error: # Re-raise Gradio errors directly
|
150 |
+
raise
|
151 |
except Exception as e:
|
152 |
+
print(f"An unexpected error occurred: {e}")
|
153 |
+
# Clean up the processing directory in case of an unhandled error
|
154 |
+
if os.path.exists(processing_temp_dir):
|
155 |
+
shutil.rmtree(processing_temp_dir)
|
156 |
+
raise gr.Error(f"An unexpected error occurred during processing: {str(e)}")
|
157 |
+
# Note: Do not clean up processing_temp_dir in a `finally` block here if returning path from it,
|
158 |
+
# as Gradio needs the file to exist to serve it. Gradio's gr.File output type handles temp file cleanup.
|
159 |
+
# If outputting gr.File, copy the final file to a new tempfile managed by Gradio.
|
160 |
+
# For gr.Model3D, path is fine.
|
161 |
|
162 |
# --- Gradio Interface ---
|
|
|
|
|
163 |
theme = gr.themes.Soft(
|
164 |
+
primary_hue=gr.themes.colors.sky,
|
165 |
+
secondary_hue=gr.themes.colors.blue,
|
166 |
+
neutral_hue=gr.themes.colors.slate,
|
167 |
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
|
|
|
|
|
|
|
|
|
168 |
)
|
169 |
|
|
|
170 |
iface = gr.Interface(
|
171 |
+
fn=rig_glb_mesh_multistep,
|
172 |
+
inputs=gr.File(label="Upload .glb Mesh File", type="file"),
|
173 |
+
outputs=gr.Model3D(
|
174 |
+
label="Rigged 3D Model (.glb)",
|
175 |
+
clear_color=[0.8, 0.8, 0.8, 1.0],
|
176 |
+
# Note: Model3D might have issues with complex GLBs or certain rigging structures.
|
177 |
+
# A gr.File output for download might be a safer fallback.
|
178 |
+
# outputs=[gr.Model3D(...), gr.File(label="Download Rigged GLB")]
|
179 |
+
),
|
180 |
+
title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
|
181 |
description=(
|
182 |
+
"Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
|
183 |
+
"The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
|
184 |
+
"This may take several minutes. Ensure your GLB has clean geometry.\n"
|
185 |
+
f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{UNIRIG_REPO_DIR}'.\n"
|
186 |
f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
|
187 |
),
|
188 |
+
cache_examples=False,
|
|
|
|
|
|
|
|
|
189 |
theme=theme,
|
190 |
allow_flagging="never"
|
191 |
)
|
192 |
|
193 |
if __name__ == "__main__":
|
194 |
+
# Perform a quick check for UniRig directory on launch
|
195 |
+
if not os.path.isdir(UNIRIG_REPO_DIR):
|
196 |
+
print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will likely fail.")
|
197 |
+
# You could display this error in the Gradio interface itself using a dummy function or Markdown.
|
198 |
+
|
199 |
+
# For local testing, you might need to set PYTHONPATH or ensure UniRig is installed.
|
200 |
+
# Example: os.environ["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{os.environ.get('PYTHONPATH', '')}"
|
201 |
+
|
202 |
iface.launch()
|