jkorstad commited on
Commit
bdeed42
·
verified ·
1 Parent(s): cc5e7fa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +162 -191
app.py CHANGED
@@ -9,82 +9,96 @@ import spaces
9
  from typing import Any, Dict, Union, List
10
 
11
  # --- Configuration ---
12
- # Path to the cloned UniRig repository directory within the Space
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" # Corresponds to Blender 4.2.x
19
- BLENDER_PYTHON_VERSION = "python3.11" # Blender 4.2 uses Python 3.11
20
 
21
- # Construct paths dynamically based on the above
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") # Directory containing python executable
24
- BLENDER_PYTHON_EXEC = os.path.join(BLENDER_PYTHON_BIN_DIR, BLENDER_PYTHON_VERSION) # Full path to executable
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 Python executable exists (needed for environment setup)
35
- # Run setup script if Blender isn't found (assuming setup script handles installation)
36
- if not os.path.exists(BLENDER_PYTHON_EXEC):
37
- print(f"Blender Python executable not found at {BLENDER_PYTHON_EXEC}. Running setup script...")
 
 
 
 
 
 
 
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
- print(f"Setup STDERR:\n{setup_result.stderr}")
46
- # Re-check if the executable exists after running the script
47
- if not os.path.exists(BLENDER_PYTHON_EXEC):
48
- raise RuntimeError(f"Setup script ran but Blender Python still not found at {BLENDER_PYTHON_EXEC}.")
 
 
 
 
 
 
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
- # If setup script is missing, raise a Gradio error
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
- # Verify Blender Python site-packages path and bpy module presence
65
- bpy_found = False
 
 
 
 
 
66
  if os.path.exists(BLENDER_PYTHON_SITE_PACKAGES):
67
  print(f"Blender Python site-packages found at: {BLENDER_PYTHON_SITE_PACKAGES}")
68
- # Check 1: 'bpy' directory with __init__.py
69
- bpy_module_dir = os.path.join(BLENDER_PYTHON_SITE_PACKAGES, "bpy")
70
- if os.path.isdir(bpy_module_dir) and os.path.exists(os.path.join(bpy_module_dir, "__init__.py")):
71
- print("Found 'bpy' module directory in site-packages.")
72
- bpy_found = True
73
- # Check 2: 'bpy.so' file (less common structure)
74
- elif os.path.exists(os.path.join(BLENDER_PYTHON_SITE_PACKAGES, "bpy.so")):
75
- print("Found 'bpy.so' in site-packages.")
76
- bpy_found = True
77
-
78
- if not bpy_found:
79
- print("WARNING: Blender Python 'bpy' module indicator not found in site-packages. Imports might fail.")
 
 
 
 
 
 
 
 
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 or not detected by PyTorch.")
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 # Don't raise error, maybe it's not needed
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
- content = content.replace("from typing import", f"{typing_import}\nfrom typing import", 1)
138
- else:
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(script_path: str, args: List[str], step_name: str):
152
  """
153
- Runs a specific UniRig SHELL script (.sh) using bash in a subprocess,
154
- ensuring the correct environment (PYTHONPATH, etc.) is set within Blender's context.
155
 
156
  Args:
157
- script_path: Absolute path to the .sh script to execute.
158
- args: A list of command-line arguments for the script.
159
  step_name: Name of the step for logging.
160
  """
161
- # Use xvfb-run to enable headless execution of Blender
162
- cmd = ["xvfb-run", "-a", "bash", script_path] + args
 
 
 
 
 
 
 
 
163
 
164
  print(f"\n--- Running UniRig Step: {step_name} ---")
165
- print(f"Command: {' '.join(cmd)}")
166
 
167
- # Prepare the environment for the subprocess
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
- BLENDER_PYTHON_SITE_PACKAGES,
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
- # Set LD_LIBRARY_PATH: Include Blender’s Python library directory
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 shell script with xvfb-run
 
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/Info):\n{result.stderr}")
 
 
 
 
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
- raise gr.Error(f"Error in UniRig '{step_name}': Script failed to import Blender's 'bpy' module. Check environment setup.")
223
  elif "ImportError: Failed to load PyTorch C extensions" in e.stderr:
224
- raise gr.Error(f"Error in UniRig '{step_name}': Script failed to load PyTorch extensions. Check environment and PyTorch installation.")
225
  else:
226
- raise gr.Error(f"Error in UniRig '{step_name}'. Check logs. Last error lines:\n{last_lines}")
 
227
 
228
  except FileNotFoundError:
229
- print(f"ERROR: Could not find executable 'bash' or script '{script_path}' for {step_name}.")
230
- raise gr.Error(f"Setup error for UniRig '{step_name}'. 'bash' or script '{os.path.basename(script_path)}' not found.")
 
 
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 .sh scripts.
246
  """
247
  try:
248
- patch_asset_py() # Attempt patch, might still be relevant
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 unexpected patch error: {e}")
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 os.path.exists(input_glb_path):
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 SHELL Scripts ---
306
- skeleton_script_path = os.path.join(UNIRIG_REPO_DIR, "launch/inference/generate_skeleton.sh")
307
- skin_script_path = os.path.join(UNIRIG_REPO_DIR, "launch/inference/generate_skin.sh")
308
- merge_script_path = os.path.join(UNIRIG_REPO_DIR, "launch/inference/merge.sh")
 
 
 
 
 
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(skeleton_script_path):
319
- raise gr.Error(f"Skeleton script not found at: {skeleton_script_path}")
320
- run_unirig_command(skeleton_script_path, skeleton_args, "Skeleton Prediction")
 
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
- if not os.path.exists(skin_script_path):
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
- # Determine which source to use based on what exists or a user choice?
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
- if not os.path.exists(merge_script_path):
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
- traceback.print_exc()
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
- # Display an error message if Gradio tries to build the UI without UniRig
393
- with gr.Blocks(theme=theme) as iface:
394
- gr.Markdown(f"# Application Error\n\n{startup_error_message}")
 
 
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
- label="Upload .glb Mesh File",
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 (invoked through shell scripts) to automatically generate a skeleton and skinning weights.\n"
411
- f"* Running main app on Python {sys.version.split()[0]}, UniRig steps use Blender's Python {BLENDER_PYTHON_VERSION} via .sh scripts.\n"
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, # Disable caching if results depend heavily on GPU state or are large
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