Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -9,82 +9,96 @@ import spaces
|
|
9 |
from typing import Any, Dict, Union, List
|
10 |
|
11 |
# --- Configuration ---
|
12 |
-
|
13 |
-
# Ensure this path is correct relative to app.py
|
14 |
-
UNIRIG_REPO_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "UniRig")) # Get absolute path
|
15 |
-
|
16 |
-
# Absolute path to the Blender installation provided in the Space environment
|
17 |
BLENDER_INSTALL_DIR = "/opt/blender-4.2.0-linux-x64"
|
18 |
-
BLENDER_PYTHON_VERSION_DIR = "4.2"
|
19 |
-
BLENDER_PYTHON_VERSION = "python3.11"
|
20 |
|
21 |
-
# Construct paths
|
22 |
BLENDER_PYTHON_DIR = os.path.join(BLENDER_INSTALL_DIR, BLENDER_PYTHON_VERSION_DIR, "python")
|
23 |
-
BLENDER_PYTHON_BIN_DIR = os.path.join(BLENDER_PYTHON_DIR, "bin")
|
24 |
-
BLENDER_PYTHON_EXEC = os.path.join(BLENDER_PYTHON_BIN_DIR, BLENDER_PYTHON_VERSION)
|
25 |
BLENDER_PYTHON_LIB_PATH = os.path.join(BLENDER_PYTHON_DIR, "lib", BLENDER_PYTHON_VERSION)
|
26 |
BLENDER_PYTHON_SITE_PACKAGES = os.path.join(BLENDER_PYTHON_LIB_PATH, "site-packages")
|
|
|
|
|
|
|
27 |
|
28 |
-
# Path to the setup script (executed only if Blender isn't found initially)
|
29 |
SETUP_SCRIPT = os.path.join(os.path.dirname(__file__), "setup_blender.sh")
|
30 |
|
31 |
# --- Initial Checks ---
|
32 |
print("--- Environment Checks ---")
|
33 |
|
34 |
-
# Check if Blender
|
35 |
-
|
36 |
-
if
|
37 |
-
print(f"Blender
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
if os.path.exists(SETUP_SCRIPT):
|
39 |
try:
|
40 |
-
# Run the setup script using bash
|
41 |
setup_result = subprocess.run(["bash", SETUP_SCRIPT], check=True, capture_output=True, text=True)
|
42 |
print("Setup script executed successfully.")
|
43 |
print(f"Setup STDOUT:\n{setup_result.stdout}")
|
44 |
-
if setup_result.stderr:
|
45 |
-
|
46 |
-
# Re-check
|
47 |
-
if
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
except subprocess.CalledProcessError as e:
|
50 |
-
print(f"ERROR running setup script: {SETUP_SCRIPT}")
|
51 |
-
print(f"Return Code: {e.returncode}")
|
52 |
-
print(f"Stdout: {e.stdout}")
|
53 |
-
print(f"Stderr: {e.stderr}")
|
54 |
-
# Raise a more informative error for Gradio if setup fails
|
55 |
raise gr.Error(f"Failed to execute setup script. Check logs. Stderr: {e.stderr[-500:]}")
|
56 |
except Exception as e:
|
57 |
raise gr.Error(f"Unexpected error running setup script '{SETUP_SCRIPT}': {e}")
|
58 |
else:
|
59 |
-
|
60 |
-
raise gr.Error(f"Blender Python not found and setup script missing: {SETUP_SCRIPT}")
|
61 |
-
else:
|
62 |
-
print(f"Blender Python executable found: {BLENDER_PYTHON_EXEC}")
|
63 |
|
64 |
-
#
|
65 |
-
|
|
|
|
|
|
|
|
|
|
|
66 |
if os.path.exists(BLENDER_PYTHON_SITE_PACKAGES):
|
67 |
print(f"Blender Python site-packages found at: {BLENDER_PYTHON_SITE_PACKAGES}")
|
68 |
-
#
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
else:
|
81 |
print(f"WARNING: Blender Python site-packages directory not found at {BLENDER_PYTHON_SITE_PACKAGES}. Check paths.")
|
82 |
|
83 |
|
84 |
# Check for UniRig repository
|
85 |
if not os.path.isdir(UNIRIG_REPO_DIR):
|
86 |
-
print(f"ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}.")
|
87 |
-
# Raise Gradio error if critical component is missing
|
88 |
raise gr.Error(f"UniRig repository missing at: {UNIRIG_REPO_DIR}. Ensure it's cloned correctly.")
|
89 |
else:
|
90 |
print(f"UniRig repository found at: {UNIRIG_REPO_DIR}")
|
@@ -99,8 +113,7 @@ if DEVICE.type == 'cuda':
|
|
99 |
except Exception as e:
|
100 |
print(f"Could not get CUDA device details: {e}")
|
101 |
else:
|
102 |
-
print("Warning: Gradio environment CUDA not available
|
103 |
-
print("UniRig subprocess will attempt to use GPU via Blender's Python environment (invoked by .sh scripts).")
|
104 |
|
105 |
print("--- End Environment Checks ---")
|
106 |
|
@@ -108,107 +121,101 @@ print("--- End Environment Checks ---")
|
|
108 |
|
109 |
def patch_asset_py():
|
110 |
"""Temporary patch to fix type hinting error in UniRig's asset.py"""
|
111 |
-
# This patch might still be needed if the .sh scripts call Python code that uses asset.py
|
112 |
asset_py_path = os.path.join(UNIRIG_REPO_DIR, "src", "data", "asset.py")
|
113 |
try:
|
114 |
-
# Check if file exists before trying to open
|
115 |
if not os.path.exists(asset_py_path):
|
116 |
print(f"Warning: asset.py not found at {asset_py_path}, skipping patch.")
|
117 |
-
return
|
118 |
-
|
119 |
-
with open(asset_py_path, "r") as f:
|
120 |
-
content = f.read()
|
121 |
|
|
|
122 |
problematic_line = "meta: Union[Dict[str, ...], None]=None"
|
123 |
corrected_line = "meta: Union[Dict[str, Any], None]=None"
|
124 |
typing_import = "from typing import Any"
|
125 |
|
126 |
if corrected_line in content:
|
127 |
-
print("Patch already applied to asset.py")
|
128 |
-
return
|
129 |
if problematic_line not in content:
|
130 |
-
print("Problematic line not found in asset.py, patch might be unnecessary.")
|
131 |
-
return
|
132 |
|
133 |
print("Applying patch to asset.py...")
|
134 |
content = content.replace(problematic_line, corrected_line)
|
135 |
if typing_import not in content:
|
136 |
-
if "from typing import" in content:
|
137 |
-
|
138 |
-
|
139 |
-
content = f"{typing_import}\n{content}"
|
140 |
-
|
141 |
-
with open(asset_py_path, "w") as f:
|
142 |
-
f.write(content)
|
143 |
print("Successfully patched asset.py")
|
144 |
|
145 |
except Exception as e:
|
146 |
-
# Log error but don't necessarily stop the app, maybe patch isn't critical
|
147 |
print(f"ERROR: Failed to patch asset.py: {e}. Proceeding cautiously.")
|
148 |
-
# raise gr.Error(f"Failed to apply necessary patch to UniRig code: {e}") # Optional: make it fatal
|
149 |
|
150 |
@spaces.GPU
|
151 |
-
def run_unirig_command(
|
152 |
"""
|
153 |
-
Runs a specific UniRig
|
154 |
-
|
155 |
|
156 |
Args:
|
157 |
-
|
158 |
-
|
159 |
step_name: Name of the step for logging.
|
160 |
"""
|
161 |
-
|
162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
|
164 |
print(f"\n--- Running UniRig Step: {step_name} ---")
|
165 |
-
print(f"Command: {' '.join(cmd)}")
|
166 |
|
167 |
-
#
|
168 |
process_env = os.environ.copy()
|
|
|
|
|
169 |
unirig_src_dir = os.path.join(UNIRIG_REPO_DIR, "src")
|
170 |
-
|
171 |
-
# Set PYTHONPATH: Blender’s site-packages + UniRig source
|
172 |
pythonpath_parts = [
|
173 |
-
|
174 |
unirig_src_dir,
|
175 |
UNIRIG_REPO_DIR
|
176 |
]
|
|
|
|
|
|
|
|
|
177 |
process_env["PYTHONPATH"] = os.pathsep.join(filter(None, pythonpath_parts))
|
178 |
print(f"Subprocess PYTHONPATH: {process_env['PYTHONPATH']}")
|
179 |
|
180 |
-
#
|
181 |
blender_lib_path = os.path.join(BLENDER_PYTHON_DIR, "lib")
|
182 |
existing_ld_path = process_env.get('LD_LIBRARY_PATH', '')
|
183 |
process_env["LD_LIBRARY_PATH"] = f"{blender_lib_path}{os.pathsep}{existing_ld_path}" if existing_ld_path else blender_lib_path
|
184 |
print(f"Subprocess LD_LIBRARY_PATH: {process_env['LD_LIBRARY_PATH']}")
|
185 |
|
186 |
-
# Set PATH to prioritize Blender's Python bin directory
|
187 |
-
existing_path = process_env.get('PATH', '')
|
188 |
-
process_env["PATH"] = f"{BLENDER_PYTHON_BIN_DIR}{os.pathsep}{existing_path}"
|
189 |
-
print(f"Subprocess PATH: {process_env['PATH']}")
|
190 |
-
|
191 |
-
# Debug: Check which Python is used
|
192 |
-
python_bin = subprocess.run(["which", "python"], env=process_env, capture_output=True, text=True)
|
193 |
-
print(f"Python binary used: {python_bin.stdout.strip()}")
|
194 |
-
|
195 |
-
# Debug: Check Blender version
|
196 |
-
blender_check = subprocess.run([os.path.join(BLENDER_INSTALL_DIR, "blender"), "--version"], capture_output=True, text=True)
|
197 |
-
print(f"Blender version: {blender_check.stdout.strip()}")
|
198 |
|
199 |
try:
|
200 |
-
# Execute the
|
|
|
201 |
result = subprocess.run(
|
202 |
cmd,
|
203 |
cwd=UNIRIG_REPO_DIR,
|
204 |
capture_output=True,
|
205 |
text=True,
|
206 |
-
check=True,
|
207 |
-
env=process_env
|
208 |
)
|
209 |
print(f"{step_name} STDOUT:\n{result.stdout}")
|
|
|
210 |
if result.stderr:
|
211 |
-
print(f"{step_name} STDERR (Warnings
|
|
|
|
|
|
|
|
|
212 |
|
213 |
except subprocess.CalledProcessError as e:
|
214 |
print(f"ERROR during {step_name}: Subprocess failed!")
|
@@ -218,16 +225,21 @@ def run_unirig_command(script_path: str, args: List[str], step_name: str):
|
|
218 |
print(f"--- {step_name} STDERR ---:\n{e.stderr}")
|
219 |
error_summary = e.stderr.strip().splitlines()
|
220 |
last_lines = "\n".join(error_summary[-5:]) if error_summary else "No stderr output."
|
|
|
|
|
221 |
if "ModuleNotFoundError: No module named 'bpy'" in e.stderr:
|
222 |
-
|
223 |
elif "ImportError: Failed to load PyTorch C extensions" in e.stderr:
|
224 |
-
|
225 |
else:
|
226 |
-
|
|
|
227 |
|
228 |
except FileNotFoundError:
|
229 |
-
|
230 |
-
|
|
|
|
|
231 |
|
232 |
except Exception as e_general:
|
233 |
print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
|
@@ -242,52 +254,19 @@ def run_unirig_command(script_path: str, args: List[str], step_name: str):
|
|
242 |
def rig_glb_mesh_multistep(input_glb_file_obj):
|
243 |
"""
|
244 |
Main Gradio function to rig a GLB mesh using UniRig's multi-step process.
|
245 |
-
Orchestrates calls to run_unirig_command for each step, executing .
|
246 |
"""
|
247 |
try:
|
248 |
-
patch_asset_py() # Attempt patch
|
249 |
-
except gr.Error as e:
|
250 |
-
print(f"Ignoring patch error: {e}") # Or just log it
|
251 |
except Exception as e:
|
252 |
-
print(f"Ignoring
|
253 |
|
254 |
# --- Input Validation ---
|
255 |
-
if input_glb_file_obj is None:
|
256 |
-
raise gr.Error("No input file provided. Please upload a .glb mesh.")
|
257 |
-
|
258 |
input_glb_path = input_glb_file_obj
|
259 |
print(f"Input GLB path received: {input_glb_path}")
|
260 |
-
|
261 |
-
if not
|
262 |
-
raise gr.Error(f"Input file path does not exist: {input_glb_path}")
|
263 |
-
if not input_glb_path.lower().endswith(".glb"):
|
264 |
-
raise gr.Error("Invalid file type. Please upload a .glb file.")
|
265 |
-
|
266 |
-
# Test PyTorch import in Blender’s Python
|
267 |
-
try:
|
268 |
-
result = subprocess.run(
|
269 |
-
[BLENDER_PYTHON_EXEC, "-c", "import torch; print(torch.__version__)"],
|
270 |
-
capture_output=True,
|
271 |
-
text=True,
|
272 |
-
check=True
|
273 |
-
)
|
274 |
-
print(f"PyTorch version in Blender's Python: {result.stdout.strip()}")
|
275 |
-
except subprocess.CalledProcessError as e:
|
276 |
-
print(f"Failed to import torch in Blender's Python: {e.stderr}")
|
277 |
-
raise gr.Error("PyTorch import failed in Blender's Python environment. Check installation.")
|
278 |
-
|
279 |
-
# Test Blender execution
|
280 |
-
try:
|
281 |
-
blender_test = subprocess.run(
|
282 |
-
[os.path.join(BLENDER_INSTALL_DIR, "blender"), "--version"],
|
283 |
-
capture_output=True,
|
284 |
-
text=True,
|
285 |
-
check=True
|
286 |
-
)
|
287 |
-
print(f"Blender version: {blender_test.stdout.strip()}")
|
288 |
-
except subprocess.CalledProcessError as e:
|
289 |
-
print(f"Failed to run Blender: {e.stderr}")
|
290 |
-
raise gr.Error("Blender is not accessible. Check installation.")
|
291 |
|
292 |
# --- Setup Temporary Directory ---
|
293 |
processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
|
@@ -296,59 +275,70 @@ def rig_glb_mesh_multistep(input_glb_file_obj):
|
|
296 |
try:
|
297 |
# --- Define File Paths ---
|
298 |
base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
|
299 |
-
# Ensure paths passed to scripts are absolute
|
300 |
abs_input_glb_path = os.path.abspath(input_glb_path)
|
301 |
abs_skeleton_output_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
|
302 |
abs_skin_output_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
|
303 |
abs_final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
|
304 |
|
305 |
-
# --- Define Absolute Paths to UniRig
|
306 |
-
|
307 |
-
|
308 |
-
|
|
|
|
|
|
|
|
|
|
|
309 |
|
310 |
# --- Execute UniRig Steps ---
|
311 |
|
312 |
# Step 1: Skeleton Prediction
|
313 |
print("\nStarting Step 1: Predicting Skeleton...")
|
|
|
|
|
314 |
skeleton_args = [
|
|
|
|
|
315 |
"--input", abs_input_glb_path,
|
316 |
"--output", abs_skeleton_output_path
|
|
|
317 |
]
|
318 |
-
if not os.path.exists(
|
319 |
-
raise gr.Error(f"
|
320 |
-
|
|
|
321 |
if not os.path.exists(abs_skeleton_output_path):
|
322 |
-
# Check if the error wasn't already raised by run_unirig_command
|
323 |
raise gr.Error("Skeleton prediction failed. Output file not created. Check logs.")
|
324 |
print("Step 1: Skeleton Prediction completed.")
|
325 |
|
326 |
# Step 2: Skinning Weight Prediction
|
327 |
print("\nStarting Step 2: Predicting Skinning Weights...")
|
|
|
|
|
328 |
skin_args = [
|
|
|
329 |
"--input", abs_skeleton_output_path, # Input is the skeleton from step 1
|
330 |
-
"--source", abs_input_glb_path, # Source mesh
|
331 |
"--output", abs_skin_output_path
|
332 |
]
|
333 |
-
|
334 |
-
raise gr.Error(f"Skinning script not found at: {skin_script_path}")
|
335 |
-
run_unirig_command(skin_script_path, skin_args, "Skinning Prediction")
|
336 |
if not os.path.exists(abs_skin_output_path):
|
337 |
raise gr.Error("Skinning prediction failed. Output file not created. Check logs.")
|
338 |
print("Step 2: Skinning Prediction completed.")
|
339 |
|
340 |
# Step 3: Merge Skeleton/Skin with Original Mesh
|
341 |
print("\nStarting Step 3: Merging Results...")
|
|
|
|
|
|
|
|
|
342 |
merge_args = [
|
343 |
-
#
|
344 |
-
# Assuming skin output is the desired source if it exists.
|
345 |
"--source", abs_skin_output_path,
|
346 |
"--target", abs_input_glb_path,
|
347 |
"--output", abs_final_rigged_glb_path
|
348 |
]
|
349 |
-
|
350 |
-
raise gr.Error(f"Merging script not found at: {merge_script_path}")
|
351 |
-
run_unirig_command(merge_script_path, merge_args, "Merging")
|
352 |
if not os.path.exists(abs_final_rigged_glb_path):
|
353 |
raise gr.Error("Merging process failed. Final rigged GLB file not created. Check logs.")
|
354 |
print("Step 3: Merging completed.")
|
@@ -359,18 +349,12 @@ def rig_glb_mesh_multistep(input_glb_file_obj):
|
|
359 |
|
360 |
except gr.Error as e:
|
361 |
print(f"Gradio Error occurred: {e}")
|
362 |
-
if os.path.exists(processing_temp_dir):
|
363 |
-
shutil.rmtree(processing_temp_dir)
|
364 |
-
print(f"Cleaned up temporary directory: {processing_temp_dir}")
|
365 |
raise e
|
366 |
-
|
367 |
except Exception as e:
|
368 |
print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
|
369 |
-
import traceback
|
370 |
-
|
371 |
-
if os.path.exists(processing_temp_dir):
|
372 |
-
shutil.rmtree(processing_temp_dir)
|
373 |
-
print(f"Cleaned up temporary directory: {processing_temp_dir}")
|
374 |
raise gr.Error(f"An unexpected error occurred during processing: {str(e)[:500]}")
|
375 |
|
376 |
|
@@ -384,47 +368,34 @@ theme = gr.themes.Soft(
|
|
384 |
|
385 |
# Check UniRig repo existence again before building the interface
|
386 |
if not os.path.isdir(UNIRIG_REPO_DIR):
|
387 |
-
startup_error_message = (
|
388 |
-
f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. "
|
389 |
-
"The application cannot start. Please ensure the repository is cloned correctly."
|
390 |
-
)
|
391 |
print(startup_error_message)
|
392 |
-
|
393 |
-
|
394 |
-
|
|
|
|
|
395 |
else:
|
396 |
# Build the normal interface if UniRig is found
|
397 |
iface = gr.Interface(
|
398 |
fn=rig_glb_mesh_multistep,
|
399 |
-
inputs=gr.File(
|
400 |
-
|
401 |
-
type="filepath", # Provides the path to the uploaded file
|
402 |
-
file_types=[".glb"] # Restrict file types
|
403 |
-
),
|
404 |
-
outputs=gr.Model3D(
|
405 |
-
label="Rigged 3D Model (.glb)",
|
406 |
-
clear_color=[0.8, 0.8, 0.8, 1.0] # Background color for the viewer
|
407 |
-
),
|
408 |
title=f"UniRig Auto-Rigger (Blender {BLENDER_PYTHON_VERSION_DIR} / Python {BLENDER_PYTHON_VERSION})",
|
409 |
description=(
|
410 |
-
"Upload a 3D mesh in `.glb` format. This application uses UniRig via Blender
|
411 |
-
f"* Running main app on Python {sys.version.split()[0]}, UniRig steps use Blender's Python {BLENDER_PYTHON_VERSION}
|
412 |
f"* Utilizing device: **{DEVICE.type.upper()}** (via ZeroGPU if available).\n"
|
413 |
f"* UniRig Source: https://github.com/VAST-AI-Research/UniRig"
|
414 |
),
|
415 |
-
cache_examples=False,
|
416 |
-
theme=theme,
|
417 |
-
allow_flagging='never' # Disable flagging unless needed
|
418 |
)
|
419 |
|
420 |
# --- Launch the Application ---
|
421 |
if __name__ == "__main__":
|
422 |
-
# Ensure the interface object exists before launching
|
423 |
if 'iface' in locals():
|
424 |
print("Launching Gradio interface...")
|
425 |
-
# Consider adding share=True for public link if needed, or server_name="0.0.0.0"
|
426 |
iface.launch()
|
427 |
else:
|
428 |
-
# This case should only happen if the UniRig repo check failed above
|
429 |
print("ERROR: Gradio interface not created due to startup errors.")
|
430 |
|
|
|
9 |
from typing import Any, Dict, Union, List
|
10 |
|
11 |
# --- Configuration ---
|
12 |
+
UNIRIG_REPO_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "UniRig"))
|
|
|
|
|
|
|
|
|
13 |
BLENDER_INSTALL_DIR = "/opt/blender-4.2.0-linux-x64"
|
14 |
+
BLENDER_PYTHON_VERSION_DIR = "4.2"
|
15 |
+
BLENDER_PYTHON_VERSION = "python3.11"
|
16 |
|
17 |
+
# Construct paths
|
18 |
BLENDER_PYTHON_DIR = os.path.join(BLENDER_INSTALL_DIR, BLENDER_PYTHON_VERSION_DIR, "python")
|
19 |
+
BLENDER_PYTHON_BIN_DIR = os.path.join(BLENDER_PYTHON_DIR, "bin")
|
20 |
+
BLENDER_PYTHON_EXEC = os.path.join(BLENDER_PYTHON_BIN_DIR, BLENDER_PYTHON_VERSION)
|
21 |
BLENDER_PYTHON_LIB_PATH = os.path.join(BLENDER_PYTHON_DIR, "lib", BLENDER_PYTHON_VERSION)
|
22 |
BLENDER_PYTHON_SITE_PACKAGES = os.path.join(BLENDER_PYTHON_LIB_PATH, "site-packages")
|
23 |
+
# Path to the main blender executable (assuming symlink exists or using direct path)
|
24 |
+
BLENDER_EXEC = os.path.join(BLENDER_INSTALL_DIR, "blender") # Use direct path first
|
25 |
+
BLENDER_EXEC_SYMLINK = "/usr/local/bin/blender" # Fallback symlink
|
26 |
|
|
|
27 |
SETUP_SCRIPT = os.path.join(os.path.dirname(__file__), "setup_blender.sh")
|
28 |
|
29 |
# --- Initial Checks ---
|
30 |
print("--- Environment Checks ---")
|
31 |
|
32 |
+
# Check if main Blender executable exists
|
33 |
+
blender_executable_to_use = None
|
34 |
+
if os.path.exists(BLENDER_EXEC):
|
35 |
+
print(f"Blender executable found at direct path: {BLENDER_EXEC}")
|
36 |
+
blender_executable_to_use = BLENDER_EXEC
|
37 |
+
elif os.path.exists(BLENDER_EXEC_SYMLINK):
|
38 |
+
print(f"Blender executable found via symlink: {BLENDER_EXEC_SYMLINK}")
|
39 |
+
blender_executable_to_use = BLENDER_EXEC_SYMLINK
|
40 |
+
else:
|
41 |
+
# Try running setup if Blender executable is missing
|
42 |
+
print(f"Blender executable not found at {BLENDER_EXEC} or {BLENDER_EXEC_SYMLINK}. Running setup script...")
|
43 |
if os.path.exists(SETUP_SCRIPT):
|
44 |
try:
|
|
|
45 |
setup_result = subprocess.run(["bash", SETUP_SCRIPT], check=True, capture_output=True, text=True)
|
46 |
print("Setup script executed successfully.")
|
47 |
print(f"Setup STDOUT:\n{setup_result.stdout}")
|
48 |
+
if setup_result.stderr: print(f"Setup STDERR:\n{setup_result.stderr}")
|
49 |
+
|
50 |
+
# Re-check for executable
|
51 |
+
if os.path.exists(BLENDER_EXEC):
|
52 |
+
blender_executable_to_use = BLENDER_EXEC
|
53 |
+
elif os.path.exists(BLENDER_EXEC_SYMLINK):
|
54 |
+
blender_executable_to_use = BLENDER_EXEC_SYMLINK
|
55 |
+
|
56 |
+
if not blender_executable_to_use:
|
57 |
+
raise RuntimeError(f"Setup script ran but Blender executable still not found at {BLENDER_EXEC} or {BLENDER_EXEC_SYMLINK}.")
|
58 |
+
|
59 |
except subprocess.CalledProcessError as e:
|
60 |
+
print(f"ERROR running setup script: {SETUP_SCRIPT}\nStderr: {e.stderr}")
|
|
|
|
|
|
|
|
|
61 |
raise gr.Error(f"Failed to execute setup script. Check logs. Stderr: {e.stderr[-500:]}")
|
62 |
except Exception as e:
|
63 |
raise gr.Error(f"Unexpected error running setup script '{SETUP_SCRIPT}': {e}")
|
64 |
else:
|
65 |
+
raise gr.Error(f"Blender executable not found and setup script missing: {SETUP_SCRIPT}")
|
|
|
|
|
|
|
66 |
|
67 |
+
# Check Python executable (still useful for checks)
|
68 |
+
if not os.path.exists(BLENDER_PYTHON_EXEC):
|
69 |
+
print(f"WARNING: Blender Python executable not found at {BLENDER_PYTHON_EXEC}, though main executable exists.")
|
70 |
+
|
71 |
+
|
72 |
+
# Verify Blender Python site-packages path and attempt bpy import test
|
73 |
+
bpy_import_ok = False
|
74 |
if os.path.exists(BLENDER_PYTHON_SITE_PACKAGES):
|
75 |
print(f"Blender Python site-packages found at: {BLENDER_PYTHON_SITE_PACKAGES}")
|
76 |
+
# Try importing bpy using blender --background --python
|
77 |
+
try:
|
78 |
+
test_script_content = "import bpy; print('bpy imported successfully')"
|
79 |
+
test_result = subprocess.run(
|
80 |
+
[blender_executable_to_use, "--background", "--python-expr", test_script_content],
|
81 |
+
capture_output=True, text=True, check=True, timeout=30 # Add timeout
|
82 |
+
)
|
83 |
+
if "bpy imported successfully" in test_result.stdout:
|
84 |
+
print("Successfully imported 'bpy' using Blender executable.")
|
85 |
+
bpy_import_ok = True
|
86 |
+
else:
|
87 |
+
print(f"WARNING: 'bpy' import test via Blender returned unexpected output:\nSTDOUT:{test_result.stdout}\nSTDERR:{test_result.stderr}")
|
88 |
+
|
89 |
+
except subprocess.TimeoutExpired:
|
90 |
+
print("WARNING: 'bpy' import test via Blender timed out.")
|
91 |
+
except subprocess.CalledProcessError as e:
|
92 |
+
print(f"WARNING: Failed to import 'bpy' using Blender executable:\nSTDOUT:{e.stdout}\nSTDERR:{e.stderr}")
|
93 |
+
except Exception as e:
|
94 |
+
print(f"WARNING: Unexpected error during 'bpy' import test: {e}")
|
95 |
+
|
96 |
else:
|
97 |
print(f"WARNING: Blender Python site-packages directory not found at {BLENDER_PYTHON_SITE_PACKAGES}. Check paths.")
|
98 |
|
99 |
|
100 |
# Check for UniRig repository
|
101 |
if not os.path.isdir(UNIRIG_REPO_DIR):
|
|
|
|
|
102 |
raise gr.Error(f"UniRig repository missing at: {UNIRIG_REPO_DIR}. Ensure it's cloned correctly.")
|
103 |
else:
|
104 |
print(f"UniRig repository found at: {UNIRIG_REPO_DIR}")
|
|
|
113 |
except Exception as e:
|
114 |
print(f"Could not get CUDA device details: {e}")
|
115 |
else:
|
116 |
+
print("Warning: Gradio environment CUDA not available.")
|
|
|
117 |
|
118 |
print("--- End Environment Checks ---")
|
119 |
|
|
|
121 |
|
122 |
def patch_asset_py():
|
123 |
"""Temporary patch to fix type hinting error in UniRig's asset.py"""
|
|
|
124 |
asset_py_path = os.path.join(UNIRIG_REPO_DIR, "src", "data", "asset.py")
|
125 |
try:
|
|
|
126 |
if not os.path.exists(asset_py_path):
|
127 |
print(f"Warning: asset.py not found at {asset_py_path}, skipping patch.")
|
128 |
+
return
|
|
|
|
|
|
|
129 |
|
130 |
+
with open(asset_py_path, "r") as f: content = f.read()
|
131 |
problematic_line = "meta: Union[Dict[str, ...], None]=None"
|
132 |
corrected_line = "meta: Union[Dict[str, Any], None]=None"
|
133 |
typing_import = "from typing import Any"
|
134 |
|
135 |
if corrected_line in content:
|
136 |
+
print("Patch already applied to asset.py"); return
|
|
|
137 |
if problematic_line not in content:
|
138 |
+
print("Problematic line not found in asset.py, patch might be unnecessary."); return
|
|
|
139 |
|
140 |
print("Applying patch to asset.py...")
|
141 |
content = content.replace(problematic_line, corrected_line)
|
142 |
if typing_import not in content:
|
143 |
+
if "from typing import" in content: content = content.replace("from typing import", f"{typing_import}\nfrom typing import", 1)
|
144 |
+
else: content = f"{typing_import}\n{content}"
|
145 |
+
with open(asset_py_path, "w") as f: f.write(content)
|
|
|
|
|
|
|
|
|
146 |
print("Successfully patched asset.py")
|
147 |
|
148 |
except Exception as e:
|
|
|
149 |
print(f"ERROR: Failed to patch asset.py: {e}. Proceeding cautiously.")
|
|
|
150 |
|
151 |
@spaces.GPU
|
152 |
+
def run_unirig_command(python_script_path: str, script_args: List[str], step_name: str):
|
153 |
"""
|
154 |
+
Runs a specific UniRig PYTHON script (.py) using the Blender executable
|
155 |
+
in background mode (`blender --background --python script.py -- args`).
|
156 |
|
157 |
Args:
|
158 |
+
python_script_path: Absolute path to the .py script to execute within Blender.
|
159 |
+
script_args: A list of command-line arguments FOR THE PYTHON SCRIPT.
|
160 |
step_name: Name of the step for logging.
|
161 |
"""
|
162 |
+
if not blender_executable_to_use:
|
163 |
+
raise gr.Error("Blender executable path could not be determined. Cannot run UniRig step.")
|
164 |
+
|
165 |
+
# Command structure: blender --background --python script.py -- script_arg1 script_arg2 ...
|
166 |
+
cmd = [
|
167 |
+
blender_executable_to_use,
|
168 |
+
"--background",
|
169 |
+
"--python", python_script_path,
|
170 |
+
"--" # Separator for script arguments
|
171 |
+
] + script_args
|
172 |
|
173 |
print(f"\n--- Running UniRig Step: {step_name} ---")
|
174 |
+
print(f"Command: {' '.join(cmd)}") # Note: Simple join for logging
|
175 |
|
176 |
+
# Environment variables might still be useful if the script imports other non-blender libs
|
177 |
process_env = os.environ.copy()
|
178 |
+
# Ensure UniRig source is findable if scripts use relative imports from repo root
|
179 |
+
# or if they import modules from UniRig/src
|
180 |
unirig_src_dir = os.path.join(UNIRIG_REPO_DIR, "src")
|
|
|
|
|
181 |
pythonpath_parts = [
|
182 |
+
# Blender's site-packages should be found automatically when run via Blender exec
|
183 |
unirig_src_dir,
|
184 |
UNIRIG_REPO_DIR
|
185 |
]
|
186 |
+
# Add existing PYTHONPATH if any
|
187 |
+
existing_pythonpath = process_env.get('PYTHONPATH', '')
|
188 |
+
if existing_pythonpath:
|
189 |
+
pythonpath_parts.append(existing_pythonpath)
|
190 |
process_env["PYTHONPATH"] = os.pathsep.join(filter(None, pythonpath_parts))
|
191 |
print(f"Subprocess PYTHONPATH: {process_env['PYTHONPATH']}")
|
192 |
|
193 |
+
# LD_LIBRARY_PATH might still be needed for non-python shared libs used by dependencies
|
194 |
blender_lib_path = os.path.join(BLENDER_PYTHON_DIR, "lib")
|
195 |
existing_ld_path = process_env.get('LD_LIBRARY_PATH', '')
|
196 |
process_env["LD_LIBRARY_PATH"] = f"{blender_lib_path}{os.pathsep}{existing_ld_path}" if existing_ld_path else blender_lib_path
|
197 |
print(f"Subprocess LD_LIBRARY_PATH: {process_env['LD_LIBRARY_PATH']}")
|
198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
|
200 |
try:
|
201 |
+
# Execute Blender with the Python script.
|
202 |
+
# cwd=UNIRIG_REPO_DIR ensures script runs relative to repo root.
|
203 |
result = subprocess.run(
|
204 |
cmd,
|
205 |
cwd=UNIRIG_REPO_DIR,
|
206 |
capture_output=True,
|
207 |
text=True,
|
208 |
+
check=True, # Raises CalledProcessError on non-zero exit codes
|
209 |
+
env=process_env # Pass the modified environment
|
210 |
)
|
211 |
print(f"{step_name} STDOUT:\n{result.stdout}")
|
212 |
+
# Blender often prints info/warnings to stderr even on success
|
213 |
if result.stderr:
|
214 |
+
print(f"{step_name} STDERR (Info/Warnings):\n{result.stderr}")
|
215 |
+
# Check stderr for common Blender errors even if exit code was 0
|
216 |
+
if "Error: Cannot read file" in result.stderr:
|
217 |
+
raise gr.Error(f"Blender reported an error reading an input file during {step_name}. Check paths and file integrity.")
|
218 |
+
# Add other specific Blender error checks if needed
|
219 |
|
220 |
except subprocess.CalledProcessError as e:
|
221 |
print(f"ERROR during {step_name}: Subprocess failed!")
|
|
|
225 |
print(f"--- {step_name} STDERR ---:\n{e.stderr}")
|
226 |
error_summary = e.stderr.strip().splitlines()
|
227 |
last_lines = "\n".join(error_summary[-5:]) if error_summary else "No stderr output."
|
228 |
+
# Check specifically for bpy/torch import errors within the subprocess stderr
|
229 |
+
# These *shouldn't* happen now, but check just in case
|
230 |
if "ModuleNotFoundError: No module named 'bpy'" in e.stderr:
|
231 |
+
raise gr.Error(f"Error in UniRig '{step_name}': Blender failed to provide 'bpy' module internally.")
|
232 |
elif "ImportError: Failed to load PyTorch C extensions" in e.stderr:
|
233 |
+
raise gr.Error(f"Error in UniRig '{step_name}': Script failed to load PyTorch extensions even within Blender. Check installation.")
|
234 |
else:
|
235 |
+
raise gr.Error(f"Error in UniRig '{step_name}'. Check logs. Last error lines:\n{last_lines}")
|
236 |
+
|
237 |
|
238 |
except FileNotFoundError:
|
239 |
+
# This error means blender executable or the python script wasn't found
|
240 |
+
print(f"ERROR: Could not find Blender executable '{blender_executable_to_use}' or script '{python_script_path}' for {step_name}.")
|
241 |
+
print(f"Attempted command: {' '.join(cmd)}")
|
242 |
+
raise gr.Error(f"Setup error for UniRig '{step_name}'. Blender or Python script not found.")
|
243 |
|
244 |
except Exception as e_general:
|
245 |
print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
|
|
|
254 |
def rig_glb_mesh_multistep(input_glb_file_obj):
|
255 |
"""
|
256 |
Main Gradio function to rig a GLB mesh using UniRig's multi-step process.
|
257 |
+
Orchestrates calls to run_unirig_command for each step, executing .py scripts via Blender.
|
258 |
"""
|
259 |
try:
|
260 |
+
patch_asset_py() # Attempt patch
|
|
|
|
|
261 |
except Exception as e:
|
262 |
+
print(f"Ignoring patch error: {e}")
|
263 |
|
264 |
# --- Input Validation ---
|
265 |
+
if input_glb_file_obj is None: raise gr.Error("No input file provided.")
|
|
|
|
|
266 |
input_glb_path = input_glb_file_obj
|
267 |
print(f"Input GLB path received: {input_glb_path}")
|
268 |
+
if not os.path.exists(input_glb_path): raise gr.Error(f"Input file path does not exist: {input_glb_path}")
|
269 |
+
if not input_glb_path.lower().endswith(".glb"): raise gr.Error("Invalid file type. Please upload a .glb file.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
270 |
|
271 |
# --- Setup Temporary Directory ---
|
272 |
processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
|
|
|
275 |
try:
|
276 |
# --- Define File Paths ---
|
277 |
base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
|
|
|
278 |
abs_input_glb_path = os.path.abspath(input_glb_path)
|
279 |
abs_skeleton_output_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
|
280 |
abs_skin_output_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
|
281 |
abs_final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
|
282 |
|
283 |
+
# --- Define Absolute Paths to UniRig PYTHON Scripts ---
|
284 |
+
# *** IMPORTANT: Assuming the .sh scripts primarily wrap these .py scripts. ***
|
285 |
+
# *** This might need adjustment based on the actual content of the .sh files. ***
|
286 |
+
# Use run.py as the main entry point, passing task-specific configs/args
|
287 |
+
# This requires inspecting run.py and the shell scripts to know the correct args/configs.
|
288 |
+
# For now, let's assume a structure like: python run.py --config <config_file> --input ... --output ...
|
289 |
+
|
290 |
+
# Example: Assuming run.py takes task name and args
|
291 |
+
run_script_path = os.path.join(UNIRIG_REPO_DIR, "run.py") # Main script?
|
292 |
|
293 |
# --- Execute UniRig Steps ---
|
294 |
|
295 |
# Step 1: Skeleton Prediction
|
296 |
print("\nStarting Step 1: Predicting Skeleton...")
|
297 |
+
# Arguments for the run.py script for skeleton task
|
298 |
+
# ** These are GUESSES based on typical structure - VERIFY from UniRig code **
|
299 |
skeleton_args = [
|
300 |
+
# Might need a config file path or task name argument first
|
301 |
+
# e.g., "--task", "generate_skeleton",
|
302 |
"--input", abs_input_glb_path,
|
303 |
"--output", abs_skeleton_output_path
|
304 |
+
# Add other relevant args like model path if needed
|
305 |
]
|
306 |
+
if not os.path.exists(run_script_path): # Check if assumed script exists
|
307 |
+
raise gr.Error(f"UniRig main script not found at: {run_script_path}")
|
308 |
+
# Execute run.py via Blender
|
309 |
+
run_unirig_command(run_script_path, skeleton_args, "Skeleton Prediction")
|
310 |
if not os.path.exists(abs_skeleton_output_path):
|
|
|
311 |
raise gr.Error("Skeleton prediction failed. Output file not created. Check logs.")
|
312 |
print("Step 1: Skeleton Prediction completed.")
|
313 |
|
314 |
# Step 2: Skinning Weight Prediction
|
315 |
print("\nStarting Step 2: Predicting Skinning Weights...")
|
316 |
+
# Arguments for the run.py script for skinning task
|
317 |
+
# ** GUESSES - VERIFY from UniRig code **
|
318 |
skin_args = [
|
319 |
+
# e.g., "--task", "generate_skin",
|
320 |
"--input", abs_skeleton_output_path, # Input is the skeleton from step 1
|
321 |
+
"--source", abs_input_glb_path, # Source mesh (if needed by skin script)
|
322 |
"--output", abs_skin_output_path
|
323 |
]
|
324 |
+
run_unirig_command(run_script_path, skin_args, "Skinning Prediction")
|
|
|
|
|
325 |
if not os.path.exists(abs_skin_output_path):
|
326 |
raise gr.Error("Skinning prediction failed. Output file not created. Check logs.")
|
327 |
print("Step 2: Skinning Prediction completed.")
|
328 |
|
329 |
# Step 3: Merge Skeleton/Skin with Original Mesh
|
330 |
print("\nStarting Step 3: Merging Results...")
|
331 |
+
# Arguments for the run.py script for merging task
|
332 |
+
# ** GUESSES - VERIFY from UniRig code **
|
333 |
+
# Alternatively, merge might use a different script like src/data/merge_fbx.py?
|
334 |
+
# Let's assume run.py handles it for now.
|
335 |
merge_args = [
|
336 |
+
# e.g., "--task", "merge",
|
|
|
337 |
"--source", abs_skin_output_path,
|
338 |
"--target", abs_input_glb_path,
|
339 |
"--output", abs_final_rigged_glb_path
|
340 |
]
|
341 |
+
run_unirig_command(run_script_path, merge_args, "Merging")
|
|
|
|
|
342 |
if not os.path.exists(abs_final_rigged_glb_path):
|
343 |
raise gr.Error("Merging process failed. Final rigged GLB file not created. Check logs.")
|
344 |
print("Step 3: Merging completed.")
|
|
|
349 |
|
350 |
except gr.Error as e:
|
351 |
print(f"Gradio Error occurred: {e}")
|
352 |
+
if os.path.exists(processing_temp_dir): shutil.rmtree(processing_temp_dir); print(f"Cleaned up temp dir: {processing_temp_dir}")
|
|
|
|
|
353 |
raise e
|
|
|
354 |
except Exception as e:
|
355 |
print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
|
356 |
+
import traceback; traceback.print_exc()
|
357 |
+
if os.path.exists(processing_temp_dir): shutil.rmtree(processing_temp_dir); print(f"Cleaned up temp dir: {processing_temp_dir}")
|
|
|
|
|
|
|
358 |
raise gr.Error(f"An unexpected error occurred during processing: {str(e)[:500]}")
|
359 |
|
360 |
|
|
|
368 |
|
369 |
# Check UniRig repo existence again before building the interface
|
370 |
if not os.path.isdir(UNIRIG_REPO_DIR):
|
371 |
+
startup_error_message = (f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}.")
|
|
|
|
|
|
|
372 |
print(startup_error_message)
|
373 |
+
with gr.Blocks(theme=theme) as iface: gr.Markdown(f"# Application Error\n\n{startup_error_message}")
|
374 |
+
elif not blender_executable_to_use:
|
375 |
+
startup_error_message = (f"CRITICAL STARTUP ERROR: Blender executable not found.")
|
376 |
+
print(startup_error_message)
|
377 |
+
with gr.Blocks(theme=theme) as iface: gr.Markdown(f"# Application Error\n\n{startup_error_message}")
|
378 |
else:
|
379 |
# Build the normal interface if UniRig is found
|
380 |
iface = gr.Interface(
|
381 |
fn=rig_glb_mesh_multistep,
|
382 |
+
inputs=gr.File(label="Upload .glb Mesh File", type="filepath", file_types=[".glb"]),
|
383 |
+
outputs=gr.Model3D(label="Rigged 3D Model (.glb)", clear_color=[0.8, 0.8, 0.8, 1.0]),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
384 |
title=f"UniRig Auto-Rigger (Blender {BLENDER_PYTHON_VERSION_DIR} / Python {BLENDER_PYTHON_VERSION})",
|
385 |
description=(
|
386 |
+
"Upload a 3D mesh in `.glb` format. This application uses UniRig via Blender's Python interface.\n"
|
387 |
+
f"* Running main app on Python {sys.version.split()[0]}, UniRig steps use Blender's Python {BLENDER_PYTHON_VERSION}.\n"
|
388 |
f"* Utilizing device: **{DEVICE.type.upper()}** (via ZeroGPU if available).\n"
|
389 |
f"* UniRig Source: https://github.com/VAST-AI-Research/UniRig"
|
390 |
),
|
391 |
+
cache_examples=False, theme=theme, allow_flagging='never'
|
|
|
|
|
392 |
)
|
393 |
|
394 |
# --- Launch the Application ---
|
395 |
if __name__ == "__main__":
|
|
|
396 |
if 'iface' in locals():
|
397 |
print("Launching Gradio interface...")
|
|
|
398 |
iface.launch()
|
399 |
else:
|
|
|
400 |
print("ERROR: Gradio interface not created due to startup errors.")
|
401 |
|