jkorstad commited on
Commit
bfd4089
·
verified ·
1 Parent(s): 5553984

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -120
app.py CHANGED
@@ -13,7 +13,6 @@ APP_ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) # Should be /home/user
13
 
14
  UNIRIG_REPO_DIR = os.path.join(APP_ROOT_DIR, "UniRig")
15
 
16
- # ** MODIFIED FOR LOCAL BLENDER INSTALLATION **
17
  BLENDER_VERSION_NAME = "blender-4.2.0-linux-x64"
18
  BLENDER_LOCAL_INSTALL_BASE_DIR = os.path.join(APP_ROOT_DIR, "blender_installation")
19
  BLENDER_INSTALL_DIR = os.path.join(BLENDER_LOCAL_INSTALL_BASE_DIR, BLENDER_VERSION_NAME)
@@ -21,20 +20,16 @@ BLENDER_INSTALL_DIR = os.path.join(BLENDER_LOCAL_INSTALL_BASE_DIR, BLENDER_VERSI
21
  BLENDER_PYTHON_VERSION_DIR = "4.2" # From Blender's internal structure
22
  BLENDER_PYTHON_VERSION = "python3.11" # UniRig requirement
23
 
24
- # Construct paths based on the new local installation
25
  BLENDER_PYTHON_DIR = os.path.join(BLENDER_INSTALL_DIR, BLENDER_PYTHON_VERSION_DIR, "python")
26
  BLENDER_PYTHON_BIN_DIR = os.path.join(BLENDER_PYTHON_DIR, "bin")
27
  BLENDER_EXEC = os.path.join(BLENDER_INSTALL_DIR, "blender")
28
 
29
- # Symlink might not be created or might be in a local bin, app.py prioritizes BLENDER_EXEC
30
- # Keeping this definition for fallback, but it's less likely to be the primary method now.
31
- BLENDER_EXEC_SYMLINK = "/usr/local/bin/blender" # Standard system symlink path (may not exist/be used)
32
- LOCAL_BIN_DIR = os.path.join(APP_ROOT_DIR, "local_bin") # Potential local symlink location
33
  BLENDER_EXEC_LOCAL_SYMLINK = os.path.join(LOCAL_BIN_DIR, "blender")
34
-
35
 
36
  SETUP_SCRIPT = os.path.join(APP_ROOT_DIR, "setup_blender.sh")
37
- SETUP_SCRIPT_TIMEOUT = 1800 # Increased timeout
38
 
39
  # --- Initial Checks ---
40
  print("--- Environment Checks ---")
@@ -49,15 +44,13 @@ if os.path.exists(BLENDER_EXEC):
49
  elif os.path.exists(BLENDER_EXEC_LOCAL_SYMLINK):
50
  print(f"Blender executable found via local symlink: {BLENDER_EXEC_LOCAL_SYMLINK}")
51
  blender_executable_to_use = BLENDER_EXEC_LOCAL_SYMLINK
52
- elif os.path.exists(BLENDER_EXEC_SYMLINK): # Fallback to system symlink (less likely)
53
  print(f"Blender executable found via system symlink: {BLENDER_EXEC_SYMLINK}")
54
  blender_executable_to_use = BLENDER_EXEC_SYMLINK
55
  else:
56
- print(f"Blender executable not found at {BLENDER_EXEC}, {BLENDER_EXEC_LOCAL_SYMLINK}, or {BLENDER_EXEC_SYMLINK}. Running setup script...")
57
  if os.path.exists(SETUP_SCRIPT):
58
  try:
59
- # Run setup script if Blender not found
60
- # Ensure setup_blender.sh is executable (chmod +x setup_blender.sh in Dockerfile or Space setup)
61
  setup_result = subprocess.run(
62
  ["bash", SETUP_SCRIPT],
63
  check=True,
@@ -69,7 +62,6 @@ else:
69
  print(f"Setup STDOUT:\n{setup_result.stdout}")
70
  if setup_result.stderr: print(f"Setup STDERR:\n{setup_result.stderr}")
71
 
72
- # Re-check for executable after running setup (prioritize direct local path)
73
  if os.path.exists(BLENDER_EXEC):
74
  blender_executable_to_use = BLENDER_EXEC
75
  print(f"Blender executable now found at direct local path: {BLENDER_EXEC}")
@@ -81,19 +73,18 @@ else:
81
  print(f"Blender executable now found via system symlink: {BLENDER_EXEC_SYMLINK}")
82
 
83
  if not blender_executable_to_use:
84
- raise RuntimeError(f"Setup script ran but Blender executable still not found at {BLENDER_EXEC} or other checked paths.")
85
  except subprocess.TimeoutExpired:
86
- print(f"ERROR: Setup script timed out after {SETUP_SCRIPT_TIMEOUT} seconds: {SETUP_SCRIPT}")
87
- raise gr.Error(f"Setup script timed out after {SETUP_SCRIPT_TIMEOUT // 60} minutes. The Space might be too slow or setup is stuck. Check full logs for the last operation.")
88
  except subprocess.CalledProcessError as e:
89
  print(f"ERROR running setup script: {SETUP_SCRIPT}\nStderr: {e.stderr}")
90
- raise gr.Error(f"Failed to execute setup script. Check logs. Stderr: {e.stderr[-500:]}")
91
  except Exception as e:
92
  raise gr.Error(f"Unexpected error running setup script '{SETUP_SCRIPT}': {e}")
93
  else:
94
  raise gr.Error(f"Blender executable not found and setup script missing: {SETUP_SCRIPT}")
95
 
96
- # Verify bpy import using the found Blender executable
97
  bpy_import_ok = False
98
  if blender_executable_to_use:
99
  try:
@@ -107,7 +98,7 @@ if blender_executable_to_use:
107
  print("Successfully imported 'bpy' using Blender executable.")
108
  bpy_import_ok = True
109
  else:
110
- print(f"WARNING: 'bpy' import test via Blender returned unexpected output:\nSTDOUT:{test_result.stdout}\nSTDERR:{test_result.stderr}")
111
  except subprocess.TimeoutExpired:
112
  print("WARNING: 'bpy' import test via Blender timed out.")
113
  except subprocess.CalledProcessError as e:
@@ -117,17 +108,16 @@ if blender_executable_to_use:
117
  else:
118
  print("WARNING: Cannot test bpy import as Blender executable was not found.")
119
 
120
- # Check for UniRig repository and run.py
121
  unirig_repo_ok = False
122
  unirig_run_py_ok = False
123
  UNIRIG_RUN_PY = os.path.join(UNIRIG_REPO_DIR, "run.py")
124
  if not os.path.isdir(UNIRIG_REPO_DIR):
125
- raise gr.Error(f"UniRig repository missing at: {UNIRIG_REPO_DIR}. Ensure it's cloned correctly.")
126
  else:
127
  print(f"UniRig repository found at: {UNIRIG_REPO_DIR}")
128
  unirig_repo_ok = True
129
  if not os.path.exists(UNIRIG_RUN_PY):
130
- raise gr.Error(f"UniRig's run.py not found at {UNIRIG_RUN_PY}. Check UniRig clone.")
131
  else:
132
  unirig_run_py_ok = True
133
 
@@ -144,7 +134,6 @@ else:
144
  print("--- End Environment Checks ---")
145
 
146
  def patch_asset_py():
147
- """Temporary patch to fix type hinting error in UniRig's asset.py"""
148
  asset_py_path = os.path.join(UNIRIG_REPO_DIR, "src", "data", "asset.py")
149
  try:
150
  if not os.path.exists(asset_py_path):
@@ -157,7 +146,7 @@ def patch_asset_py():
157
  if corrected_line in content:
158
  print("Patch already applied to asset.py"); return
159
  if problematic_line not in content:
160
- print("Problematic line not found in asset.py, patch might be unnecessary or file changed."); return
161
  print("Applying patch to asset.py...")
162
  content = content.replace(problematic_line, corrected_line)
163
  if typing_import not in content:
@@ -172,57 +161,111 @@ def patch_asset_py():
172
 
173
  @spaces.GPU
174
  def run_unirig_command(python_script_path: str, script_args: List[str], step_name: str):
175
- """
176
- Runs a specific UniRig PYTHON script (.py) using the Blender executable
177
- in background mode (`blender --background --python script.py -- args`).
178
- """
179
  if not blender_executable_to_use:
180
- raise gr.Error("Blender executable path could not be determined. Cannot run UniRig step.")
181
 
182
  process_env = os.environ.copy()
 
 
183
  unirig_src_dir = os.path.join(UNIRIG_REPO_DIR, 'src')
184
- pythonpath_parts = [UNIRIG_REPO_DIR, unirig_src_dir]
185
  existing_pythonpath = process_env.get('PYTHONPATH', '')
186
  if existing_pythonpath:
187
  pythonpath_parts.append(existing_pythonpath)
188
  process_env["PYTHONPATH"] = os.pathsep.join(filter(None, pythonpath_parts))
189
- print(f"Subprocess PYTHONPATH: {process_env['PYTHONPATH']}")
190
-
191
- # LD_LIBRARY_PATH: Inherit system path and add Blender's libraries (now from local install)
192
- blender_main_lib_path = os.path.join(BLENDER_INSTALL_DIR, "lib") # Path to Blender's own .so files
193
- blender_python_lib_path = os.path.join(BLENDER_PYTHON_DIR, "lib") # Path to Blender's Python's site-packages etc.
194
 
 
 
195
  ld_path_parts = []
196
  if os.path.exists(blender_main_lib_path): ld_path_parts.append(blender_main_lib_path)
197
  if os.path.exists(blender_python_lib_path): ld_path_parts.append(blender_python_lib_path)
198
-
199
- # Add local bin to PATH if it exists and contains the symlink, for consistency
200
- # Also ensure Blender's own script/bin directories are implicitly available if needed by Blender.
201
- # The direct call to blender_executable_to_use should handle most cases.
202
- if os.path.isdir(LOCAL_BIN_DIR):
203
- process_env["PATH"] = f"{LOCAL_BIN_DIR}{os.pathsep}{process_env.get('PATH', '')}"
204
-
205
-
206
  existing_ld_path = process_env.get('LD_LIBRARY_PATH', '')
207
  if existing_ld_path: ld_path_parts.append(existing_ld_path)
208
  if ld_path_parts:
209
  process_env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, ld_path_parts))
210
  print(f"Subprocess LD_LIBRARY_PATH: {process_env.get('LD_LIBRARY_PATH', 'Not set')}")
 
 
 
211
  print(f"Subprocess PATH: {process_env.get('PATH', 'Not set')}")
212
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
- cmd = [
215
- blender_executable_to_use,
216
- "--background",
217
- "--python", python_script_path,
218
- "--"
219
- ] + script_args
220
- print(f"\n--- Running UniRig Step: {step_name} ---")
221
- print(f"Command: {' '.join(cmd)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  result = subprocess.run(
224
  cmd,
225
- cwd=UNIRIG_REPO_DIR,
226
  capture_output=True,
227
  text=True,
228
  check=True,
@@ -234,61 +277,65 @@ def run_unirig_command(python_script_path: str, script_args: List[str], step_nam
234
  print(f"{step_name} STDERR (Info/Warnings):\n{result.stderr}")
235
  stderr_lower = result.stderr.lower()
236
  if "error" in stderr_lower or "failed" in stderr_lower or "traceback" in stderr_lower:
237
- print(f"WARNING: Potential error messages found in STDERR for {step_name} despite success exit code.")
 
 
 
 
 
 
 
238
  except subprocess.TimeoutExpired:
239
  print(f"ERROR: {step_name} timed out after 30 minutes.")
240
- raise gr.Error(f"Processing step '{step_name}' timed out. Please try with a simpler model or check logs.")
241
  except subprocess.CalledProcessError as e:
242
  print(f"ERROR during {step_name}: Subprocess failed!")
243
  print(f"Command: {' '.join(e.cmd)}")
244
  print(f"Return code: {e.returncode}")
245
- print(f"--- {step_name} STDOUT ---:\n{e.stdout}")
246
- print(f"--- {step_name} STDERR ---:\n{e.stderr}")
 
 
247
  error_summary = e.stderr.strip().splitlines()
248
- last_lines = "\n".join(error_summary[-15:]) if error_summary else "No stderr output."
249
  specific_error = "Unknown error."
250
- if "ModuleNotFoundError: No module named 'src'" in e.stderr:
251
- specific_error = "UniRig script failed to import its own 'src' module. Check PYTHONPATH and CWD for the subprocess. See diagnostic info above."
252
  elif "ModuleNotFoundError: No module named 'bpy'" in e.stderr:
253
- specific_error = "The 'bpy' module could not be imported by the script. Check installation in Blender's Python (via setup_blender.sh)."
254
- elif "ModuleNotFoundError: No module named 'flash_attn'" in e.stderr:
255
- specific_error = "The 'flash_attn' module is missing or failed to import. It might have failed during installation. Check setup_blender.sh logs."
256
- elif "ModuleNotFoundError" in e.stderr or "ImportError" in e.stderr:
257
- specific_error = f"An import error occurred. Check library installations. Details: {last_lines}"
258
- elif "OutOfMemoryError" in e.stderr or "CUDA out of memory" in e.stderr:
259
- specific_error = "CUDA out of memory. Try a smaller model or a Space with more GPU RAM."
260
- elif "hydra.errors.ConfigCompositionException" in e.stderr:
261
- specific_error = f"Hydra configuration error. Check the arguments passed to run.py: {script_args}. Details: {last_lines}"
262
- elif "Error: Cannot read file" in e.stderr:
263
- specific_error = f"Blender could not read an input/output file. Check paths. Details: {last_lines}"
264
- elif "Error:" in e.stderr:
265
- specific_error = f"Blender reported an error. Details: {last_lines}"
266
  else:
267
  specific_error = f"Check logs. Last error lines:\n{last_lines}"
268
  raise gr.Error(f"Error in UniRig '{step_name}'. {specific_error}")
269
  except FileNotFoundError:
270
- print(f"ERROR: Could not find Blender executable '{blender_executable_to_use}' or script '{python_script_path}' for {step_name}.")
271
- raise gr.Error(f"Setup error for UniRig '{step_name}'. Blender or Python script not found.")
272
  except Exception as e_general:
273
- print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
274
  import traceback
275
  traceback.print_exc()
276
- raise gr.Error(f"Unexpected Python error during '{step_name}' execution: {str(e_general)[:500]}")
277
- print(f"--- Finished UniRig Step: {step_name} ---")
 
 
 
 
 
 
 
 
 
278
 
279
  @spaces.GPU
280
  def rig_glb_mesh_multistep(input_glb_file_obj):
281
- """
282
- Main Gradio function to rig a GLB mesh using UniRig's multi-step process.
283
- """
284
  if not blender_executable_to_use:
285
- gr.Warning("System not ready: Blender executable not found. Please wait or check logs.")
286
- return None, None
287
  if not unirig_repo_ok or not unirig_run_py_ok:
288
- gr.Warning("System not ready: UniRig repository or run.py script not found. Check setup.")
289
- return None, None
290
  if not bpy_import_ok:
291
- gr.Warning("System warning: Initial 'bpy' import test failed. Attempting to proceed, but errors may occur.")
292
 
293
  try:
294
  patch_asset_py()
@@ -298,26 +345,33 @@ def rig_glb_mesh_multistep(input_glb_file_obj):
298
  if input_glb_file_obj is None:
299
  gr.Info("Please upload a .glb file first.")
300
  return None
301
- input_glb_path = input_glb_file_obj # Gradio File component with type="filepath" returns a string path
302
  print(f"Input GLB path received: {input_glb_path}")
303
 
304
  if not isinstance(input_glb_path, str) or not os.path.exists(input_glb_path):
305
- raise gr.Error(f"Invalid input file path received or file does not exist: {input_glb_path}")
306
  if not input_glb_path.lower().endswith(".glb"):
307
  raise gr.Error("Invalid file type. Please upload a .glb file.")
308
 
 
 
 
 
309
  processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
310
  print(f"Using temporary processing directory: {processing_temp_dir}")
 
 
 
311
  try:
312
  base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
313
- # Ensure paths used by UniRig are absolute, especially if its CWD changes
314
  abs_input_glb_path = os.path.abspath(input_glb_path)
315
  abs_skeleton_output_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
316
  abs_skin_output_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
317
  abs_final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
318
- unirig_script_to_run = UNIRIG_RUN_PY
319
 
320
- print("\n--- Running Blender Python Environment Diagnostic Test ---")
 
321
  diagnostic_script_content = f"""
322
  import sys
323
  import os
@@ -331,30 +385,25 @@ print("\\nPYTHONPATH Environment Variable (as seen by script):")
331
  print(os.environ.get('PYTHONPATH', 'PYTHONPATH not set or empty'))
332
  print("\\nLD_LIBRARY_PATH Environment Variable (as seen by script):")
333
  print(os.environ.get('LD_LIBRARY_PATH', 'LD_LIBRARY_PATH not set or empty'))
334
- print("\\nPATH Environment Variable (as seen by script):")
335
- print(os.environ.get('PATH', 'PATH not set or empty'))
336
  print("\\n--- Attempting Imports ---")
337
  try:
338
  import bpy
339
  print("SUCCESS: 'bpy' imported.")
340
- except ImportError as e:
341
- print(f"FAILED to import 'bpy': {{e}}")
342
- traceback.print_exc()
343
  except Exception as e:
344
- print(f"FAILED to import 'bpy' with other error: {{e}}")
345
  traceback.print_exc()
346
  try:
347
  print("\\nChecking for 'src' in CWD (should be UniRig repo root):")
348
- if os.path.isdir('src'):
349
  print(" 'src' directory FOUND in CWD.")
350
  if os.path.isfile(os.path.join('src', '__init__.py')):
351
  print(" 'src/__init__.py' FOUND.")
352
  else:
353
- print(" WARNING: 'src/__init__.py' NOT FOUND. 'src' may not be treated as a package.")
354
  else:
355
  print(" 'src' directory NOT FOUND in CWD.")
356
  print("\\nAttempting: from src.inference.download import download")
357
- from src.inference.download import download # Example import
358
  print("SUCCESS: 'from src.inference.download import download' worked.")
359
  except ImportError as e:
360
  print(f"FAILED: 'from src.inference.download import download': {{e}}")
@@ -364,17 +413,13 @@ except Exception as e:
364
  traceback.print_exc()
365
  print("--- End Diagnostic Info ---")
366
  """
 
367
  diagnostic_script_path = os.path.join(processing_temp_dir, "env_diagnostic_test.py")
368
  with open(diagnostic_script_path, "w") as f: f.write(diagnostic_script_content)
369
- try:
370
- run_unirig_command(diagnostic_script_path, [], "Blender Env Diagnostic")
371
- print("--- Finished Blender Python Environment Diagnostic Test ---\n")
372
- except Exception as e_diag:
373
- print(f"ERROR during diagnostic test execution: {e_diag}")
374
- if os.path.exists(diagnostic_script_path): os.remove(diagnostic_script_path)
375
- raise gr.Error(f"Blender environment diagnostic failed. Cannot proceed. Check logs. Error: {str(e_diag)[:500]}")
376
- finally:
377
- if os.path.exists(diagnostic_script_path): os.remove(diagnostic_script_path)
378
 
379
  unirig_device_arg = "device=cpu"
380
  if DEVICE.type == 'cuda':
@@ -423,14 +468,22 @@ print("--- End Diagnostic Info ---")
423
  return gr.update(value=abs_final_rigged_glb_path)
424
  except gr.Error as e:
425
  print(f"A Gradio Error occurred: {e}")
426
- if os.path.exists(processing_temp_dir): shutil.rmtree(processing_temp_dir)
427
- raise e # Re-raise Gradio errors to be displayed in UI
 
428
  except Exception as e:
429
  print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
430
  import traceback; traceback.print_exc()
431
- if os.path.exists(processing_temp_dir): shutil.rmtree(processing_temp_dir)
432
- raise gr.Error(f"An unexpected error occurred: {str(e)[:500]}. Check logs for details.")
433
  finally:
 
 
 
 
 
 
 
 
434
  if os.path.exists(processing_temp_dir):
435
  try:
436
  shutil.rmtree(processing_temp_dir)
@@ -447,7 +500,7 @@ theme = gr.themes.Soft(
447
 
448
  startup_error_message = None
449
  if not blender_executable_to_use:
450
- startup_error_message = (f"CRITICAL STARTUP ERROR: Blender executable could not be located or setup failed. Check logs. Expected at {BLENDER_EXEC}")
451
  elif not unirig_repo_ok:
452
  startup_error_message = (f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}.")
453
  elif not unirig_run_py_ok:
@@ -456,16 +509,16 @@ elif not unirig_run_py_ok:
456
  if startup_error_message:
457
  print(startup_error_message)
458
  with gr.Blocks(theme=theme) as iface:
459
- gr.Markdown(f"# Application Startup Error\n\n{startup_error_message}\n\nPlease check the Space logs for more details.")
460
  else:
461
  with gr.Blocks(theme=theme) as iface:
462
  gr.Markdown(
463
  f"""
464
  # UniRig Auto-Rigger (Blender {BLENDER_PYTHON_VERSION_DIR} / Python {BLENDER_PYTHON_VERSION})
465
- Upload a 3D mesh in `.glb` format. This application uses UniRig via Blender's Python interface to predict skeleton and skinning weights.
466
- * Running main app on Python `{sys.version.split()[0]}`, UniRig steps use Blender's Python `{BLENDER_PYTHON_VERSION}`.
467
- * Utilizing device: **{DEVICE.type.upper()}** (via ZeroGPU if available).
468
- * Blender executable: `{blender_executable_to_use}`.
469
  * UniRig Source: [https://github.com/VAST-AI-Research/UniRig](https://github.com/VAST-AI-Research/UniRig)
470
  """
471
  )
@@ -473,7 +526,7 @@ else:
473
  with gr.Column(scale=1):
474
  input_model = gr.File(
475
  label="Upload .glb Mesh File",
476
- type="filepath", # Returns a string path to a temporary copy
477
  file_types=[".glb"]
478
  )
479
  submit_button = gr.Button("Rig Model", variant="primary")
@@ -493,5 +546,5 @@ if __name__ == "__main__":
493
  print("Launching Gradio interface...")
494
  iface.launch(share=False, ssr_mode=False)
495
  else:
496
- print("ERROR: Gradio interface could not be created due to startup errors. Check logs above.")
497
 
 
13
 
14
  UNIRIG_REPO_DIR = os.path.join(APP_ROOT_DIR, "UniRig")
15
 
 
16
  BLENDER_VERSION_NAME = "blender-4.2.0-linux-x64"
17
  BLENDER_LOCAL_INSTALL_BASE_DIR = os.path.join(APP_ROOT_DIR, "blender_installation")
18
  BLENDER_INSTALL_DIR = os.path.join(BLENDER_LOCAL_INSTALL_BASE_DIR, BLENDER_VERSION_NAME)
 
20
  BLENDER_PYTHON_VERSION_DIR = "4.2" # From Blender's internal structure
21
  BLENDER_PYTHON_VERSION = "python3.11" # UniRig requirement
22
 
 
23
  BLENDER_PYTHON_DIR = os.path.join(BLENDER_INSTALL_DIR, BLENDER_PYTHON_VERSION_DIR, "python")
24
  BLENDER_PYTHON_BIN_DIR = os.path.join(BLENDER_PYTHON_DIR, "bin")
25
  BLENDER_EXEC = os.path.join(BLENDER_INSTALL_DIR, "blender")
26
 
27
+ LOCAL_BIN_DIR = os.path.join(APP_ROOT_DIR, "local_bin")
 
 
 
28
  BLENDER_EXEC_LOCAL_SYMLINK = os.path.join(LOCAL_BIN_DIR, "blender")
29
+ BLENDER_EXEC_SYMLINK = "/usr/local/bin/blender" # Fallback system symlink
30
 
31
  SETUP_SCRIPT = os.path.join(APP_ROOT_DIR, "setup_blender.sh")
32
+ SETUP_SCRIPT_TIMEOUT = 800
33
 
34
  # --- Initial Checks ---
35
  print("--- Environment Checks ---")
 
44
  elif os.path.exists(BLENDER_EXEC_LOCAL_SYMLINK):
45
  print(f"Blender executable found via local symlink: {BLENDER_EXEC_LOCAL_SYMLINK}")
46
  blender_executable_to_use = BLENDER_EXEC_LOCAL_SYMLINK
47
+ elif os.path.exists(BLENDER_EXEC_SYMLINK):
48
  print(f"Blender executable found via system symlink: {BLENDER_EXEC_SYMLINK}")
49
  blender_executable_to_use = BLENDER_EXEC_SYMLINK
50
  else:
51
+ print(f"Blender executable not found. Running setup script...")
52
  if os.path.exists(SETUP_SCRIPT):
53
  try:
 
 
54
  setup_result = subprocess.run(
55
  ["bash", SETUP_SCRIPT],
56
  check=True,
 
62
  print(f"Setup STDOUT:\n{setup_result.stdout}")
63
  if setup_result.stderr: print(f"Setup STDERR:\n{setup_result.stderr}")
64
 
 
65
  if os.path.exists(BLENDER_EXEC):
66
  blender_executable_to_use = BLENDER_EXEC
67
  print(f"Blender executable now found at direct local path: {BLENDER_EXEC}")
 
73
  print(f"Blender executable now found via system symlink: {BLENDER_EXEC_SYMLINK}")
74
 
75
  if not blender_executable_to_use:
76
+ raise RuntimeError(f"Setup script ran but Blender executable still not found.")
77
  except subprocess.TimeoutExpired:
78
+ print(f"ERROR: Setup script timed out: {SETUP_SCRIPT}")
79
+ raise gr.Error(f"Setup script timed out. Check logs.")
80
  except subprocess.CalledProcessError as e:
81
  print(f"ERROR running setup script: {SETUP_SCRIPT}\nStderr: {e.stderr}")
82
+ raise gr.Error(f"Failed to execute setup script. Stderr: {e.stderr[-500:]}")
83
  except Exception as e:
84
  raise gr.Error(f"Unexpected error running setup script '{SETUP_SCRIPT}': {e}")
85
  else:
86
  raise gr.Error(f"Blender executable not found and setup script missing: {SETUP_SCRIPT}")
87
 
 
88
  bpy_import_ok = False
89
  if blender_executable_to_use:
90
  try:
 
98
  print("Successfully imported 'bpy' using Blender executable.")
99
  bpy_import_ok = True
100
  else:
101
+ print(f"WARNING: 'bpy' import test unexpected output:\nSTDOUT:{test_result.stdout}\nSTDERR:{test_result.stderr}")
102
  except subprocess.TimeoutExpired:
103
  print("WARNING: 'bpy' import test via Blender timed out.")
104
  except subprocess.CalledProcessError as e:
 
108
  else:
109
  print("WARNING: Cannot test bpy import as Blender executable was not found.")
110
 
 
111
  unirig_repo_ok = False
112
  unirig_run_py_ok = False
113
  UNIRIG_RUN_PY = os.path.join(UNIRIG_REPO_DIR, "run.py")
114
  if not os.path.isdir(UNIRIG_REPO_DIR):
115
+ raise gr.Error(f"UniRig repository missing at: {UNIRIG_REPO_DIR}.")
116
  else:
117
  print(f"UniRig repository found at: {UNIRIG_REPO_DIR}")
118
  unirig_repo_ok = True
119
  if not os.path.exists(UNIRIG_RUN_PY):
120
+ raise gr.Error(f"UniRig's run.py not found at {UNIRIG_RUN_PY}.")
121
  else:
122
  unirig_run_py_ok = True
123
 
 
134
  print("--- End Environment Checks ---")
135
 
136
  def patch_asset_py():
 
137
  asset_py_path = os.path.join(UNIRIG_REPO_DIR, "src", "data", "asset.py")
138
  try:
139
  if not os.path.exists(asset_py_path):
 
146
  if corrected_line in content:
147
  print("Patch already applied to asset.py"); return
148
  if problematic_line not in content:
149
+ print("Problematic line not found in asset.py, patch might be unnecessary."); return
150
  print("Applying patch to asset.py...")
151
  content = content.replace(problematic_line, corrected_line)
152
  if typing_import not in content:
 
161
 
162
  @spaces.GPU
163
  def run_unirig_command(python_script_path: str, script_args: List[str], step_name: str):
 
 
 
 
164
  if not blender_executable_to_use:
165
+ raise gr.Error("Blender executable path not determined. Cannot run UniRig step.")
166
 
167
  process_env = os.environ.copy()
168
+ # PYTHONPATH is set here, but Blender's Python might not fully utilize it for sys.path initialization
169
+ # as expected. The bootstrap script below will directly manipulate sys.path.
170
  unirig_src_dir = os.path.join(UNIRIG_REPO_DIR, 'src')
171
+ pythonpath_parts = [UNIRIG_REPO_DIR, unirig_src_dir, APP_ROOT_DIR] # Added APP_ROOT_DIR for good measure
172
  existing_pythonpath = process_env.get('PYTHONPATH', '')
173
  if existing_pythonpath:
174
  pythonpath_parts.append(existing_pythonpath)
175
  process_env["PYTHONPATH"] = os.pathsep.join(filter(None, pythonpath_parts))
176
+ print(f"Subprocess PYTHONPATH (for Blender env): {process_env['PYTHONPATH']}")
 
 
 
 
177
 
178
+ blender_main_lib_path = os.path.join(BLENDER_INSTALL_DIR, "lib")
179
+ blender_python_lib_path = os.path.join(BLENDER_PYTHON_DIR, "lib")
180
  ld_path_parts = []
181
  if os.path.exists(blender_main_lib_path): ld_path_parts.append(blender_main_lib_path)
182
  if os.path.exists(blender_python_lib_path): ld_path_parts.append(blender_python_lib_path)
 
 
 
 
 
 
 
 
183
  existing_ld_path = process_env.get('LD_LIBRARY_PATH', '')
184
  if existing_ld_path: ld_path_parts.append(existing_ld_path)
185
  if ld_path_parts:
186
  process_env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, ld_path_parts))
187
  print(f"Subprocess LD_LIBRARY_PATH: {process_env.get('LD_LIBRARY_PATH', 'Not set')}")
188
+
189
+ if os.path.isdir(LOCAL_BIN_DIR):
190
+ process_env["PATH"] = f"{LOCAL_BIN_DIR}{os.pathsep}{process_env.get('PATH', '')}"
191
  print(f"Subprocess PATH: {process_env.get('PATH', 'Not set')}")
192
 
193
+ # --- Create a bootstrap script to set sys.path correctly inside Blender's Python ---
194
+ bootstrap_content = f"""
195
+ import sys
196
+ import os
197
+ import runpy
198
+
199
+ # Path to the UniRig repository root
200
+ # This directory needs to be in sys.path for 'from src...' imports to work
201
+ unirig_repo_dir_abs = '{os.path.abspath(UNIRIG_REPO_DIR)}'
202
+
203
+ # Path to the actual Python script Blender should run (e.g., UniRig's run.py or a diagnostic script)
204
+ original_script_to_run = '{os.path.abspath(python_script_path)}'
205
 
206
+ print(f"[Bootstrap] Original sys.path: {{sys.path}}", file=sys.stderr)
207
+ print(f"[Bootstrap] Current working directory: {{os.getcwd()}}", file=sys.stderr)
208
+ print(f"[Bootstrap] UNIRIG_REPO_DIR to add: {{unirig_repo_dir_abs}}", file=sys.stderr)
209
+ print(f"[Bootstrap] Original script to run: {{original_script_to_run}}", file=sys.stderr)
210
+
211
+ # Ensure the UniRig repository directory is at the beginning of sys.path
212
+ if unirig_repo_dir_abs not in sys.path:
213
+ sys.path.insert(0, unirig_repo_dir_abs)
214
+ print(f"[Bootstrap] Modified sys.path: {{sys.path}}", file=sys.stderr)
215
+ else:
216
+ print(f"[Bootstrap] UNIRIG_REPO_DIR already in sys.path.", file=sys.stderr)
217
+
218
+ # Blender passes arguments to the --python script after a '--' separator in the main command.
219
+ # sys.argv for this bootstrap script will be: [bootstrap_script_path, '--'] + original_script_args
220
+ # We need to reconstruct sys.argv for the 'original_script_to_run'.
221
+ try:
222
+ separator_index = sys.argv.index('--')
223
+ args_for_original_script = sys.argv[separator_index + 1:]
224
+ except ValueError:
225
+ # This case should not happen if app.py always adds '--'
226
+ args_for_original_script = []
227
+
228
+ sys.argv = [original_script_to_run] + args_for_original_script
229
+ print(f"[Bootstrap] Executing: {{original_script_to_run}} with sys.argv: {{sys.argv}}", file=sys.stderr)
230
+
231
+ # Change CWD to UNIRIG_REPO_DIR just before running the script, if not already set.
232
+ # The subprocess call in app.py should already set cwd=UNIRIG_REPO_DIR.
233
+ # This is a safeguard or for clarity.
234
+ if os.getcwd() != unirig_repo_dir_abs:
235
+ print(f"[Bootstrap] Changing CWD from {{os.getcwd()}} to {{unirig_repo_dir_abs}}", file=sys.stderr)
236
+ os.chdir(unirig_repo_dir_abs)
237
+
238
+ # Execute the original script
239
+ try:
240
+ runpy.run_path(original_script_to_run, run_name='__main__')
241
+ except Exception as e_runpy:
242
+ print(f"[Bootstrap] Error running '{{original_script_to_run}}' with runpy: {{e_runpy}}", file=sys.stderr)
243
+ import traceback
244
+ traceback.print_exc(file=sys.stderr)
245
+ raise # Re-raise the exception to ensure the calling process sees failure
246
+ """
247
+ temp_bootstrap_file = None # Define outside try block for visibility in finally
248
  try:
249
+ # Use a named temporary file that Blender can access
250
+ temp_bootstrap_file = tempfile.NamedTemporaryFile(mode='w', delete=False, prefix="blender_bootstrap_", suffix=".py")
251
+ temp_bootstrap_file.write(bootstrap_content)
252
+ temp_bootstrap_file.close() # Close the file so Blender can open it
253
+ bootstrap_script_path_for_blender = temp_bootstrap_file.name
254
+ print(f"Using temporary bootstrap script: {bootstrap_script_path_for_blender}")
255
+
256
+ cmd = [
257
+ blender_executable_to_use,
258
+ "--background",
259
+ "--python", bootstrap_script_path_for_blender, # Blender executes our bootstrap script
260
+ "--" # Separator for Blender args vs. bootstrap/target script args
261
+ ] + script_args # These args are passed to the bootstrap script, which then passes them to the target
262
+
263
+ print(f"\n--- Running UniRig Step (via bootstrap): {step_name} ---")
264
+ print(f"Command: {' '.join(cmd)}")
265
+
266
  result = subprocess.run(
267
  cmd,
268
+ cwd=UNIRIG_REPO_DIR, # CWD for the Blender process
269
  capture_output=True,
270
  text=True,
271
  check=True,
 
277
  print(f"{step_name} STDERR (Info/Warnings):\n{result.stderr}")
278
  stderr_lower = result.stderr.lower()
279
  if "error" in stderr_lower or "failed" in stderr_lower or "traceback" in stderr_lower:
280
+ # Check for specific bootstrap errors first
281
+ if "[bootstrap] error running" in result.stderr.lower():
282
+ print(f"ERROR: Bootstrap script reported an error running the target script for {step_name}.")
283
+ elif "module 'src' not found" in result.stderr.lower() or "no module named 'src'" in result.stderr.lower() : # Check after bootstrap
284
+ print(f"ERROR: 'src' module still not found for {step_name} even after bootstrap. Check sys.path in logs.")
285
+ else:
286
+ print(f"WARNING: Potential error messages found in STDERR for {step_name} despite success exit code.")
287
+
288
  except subprocess.TimeoutExpired:
289
  print(f"ERROR: {step_name} timed out after 30 minutes.")
290
+ raise gr.Error(f"Processing step '{step_name}' timed out.")
291
  except subprocess.CalledProcessError as e:
292
  print(f"ERROR during {step_name}: Subprocess failed!")
293
  print(f"Command: {' '.join(e.cmd)}")
294
  print(f"Return code: {e.returncode}")
295
+ # Print full stdout/stderr from the error for more context
296
+ print(f"--- {step_name} STDOUT (on error) ---:\n{e.stdout}")
297
+ print(f"--- {step_name} STDERR (on error) ---:\n{e.stderr}")
298
+
299
  error_summary = e.stderr.strip().splitlines()
300
+ last_lines = "\n".join(error_summary[-25:]) if error_summary else "No stderr output." # Increased lines
301
  specific_error = "Unknown error."
302
+ if "module 'src' not found" in e.stderr.lower() or "no module named 'src'" in e.stderr.lower() :
303
+ specific_error = "UniRig script failed to import 'src' module. Bootstrap sys.path modification might have failed. Check diagnostic logs for sys.path content within Blender's Python."
304
  elif "ModuleNotFoundError: No module named 'bpy'" in e.stderr:
305
+ specific_error = "The 'bpy' module could not be imported. Check Blender Python setup."
306
+ # ... (other specific error checks remain the same) ...
 
 
 
 
 
 
 
 
 
 
 
307
  else:
308
  specific_error = f"Check logs. Last error lines:\n{last_lines}"
309
  raise gr.Error(f"Error in UniRig '{step_name}'. {specific_error}")
310
  except FileNotFoundError:
311
+ print(f"ERROR: Could not find Blender executable or script for {step_name}.")
312
+ raise gr.Error(f"Setup error for UniRig '{step_name}'. Files not found.")
313
  except Exception as e_general:
314
+ print(f"An unexpected Python exception in run_unirig_command for {step_name}: {e_general}")
315
  import traceback
316
  traceback.print_exc()
317
+ raise gr.Error(f"Unexpected Python error during '{step_name}': {str(e_general)[:500]}")
318
+ finally:
319
+ # Clean up the temporary bootstrap script
320
+ if temp_bootstrap_file and os.path.exists(temp_bootstrap_file.name):
321
+ try:
322
+ os.remove(temp_bootstrap_file.name)
323
+ print(f"Cleaned up temporary bootstrap script: {temp_bootstrap_file.name}")
324
+ except Exception as cleanup_e:
325
+ print(f"Error cleaning up bootstrap script {temp_bootstrap_file.name}: {cleanup_e}")
326
+ print(f"--- Finished UniRig Step (via bootstrap): {step_name} ---")
327
+
328
 
329
  @spaces.GPU
330
  def rig_glb_mesh_multistep(input_glb_file_obj):
 
 
 
331
  if not blender_executable_to_use:
332
+ gr.Warning("System not ready: Blender executable not found.")
333
+ return None
334
  if not unirig_repo_ok or not unirig_run_py_ok:
335
+ gr.Warning("System not ready: UniRig repository or run.py script not found.")
336
+ return None
337
  if not bpy_import_ok:
338
+ gr.Warning("System warning: Initial 'bpy' import test failed. Proceeding cautiously.")
339
 
340
  try:
341
  patch_asset_py()
 
345
  if input_glb_file_obj is None:
346
  gr.Info("Please upload a .glb file first.")
347
  return None
348
+ input_glb_path = input_glb_file_obj
349
  print(f"Input GLB path received: {input_glb_path}")
350
 
351
  if not isinstance(input_glb_path, str) or not os.path.exists(input_glb_path):
352
+ raise gr.Error(f"Invalid input file path or file does not exist: {input_glb_path}")
353
  if not input_glb_path.lower().endswith(".glb"):
354
  raise gr.Error("Invalid file type. Please upload a .glb file.")
355
 
356
+ # Use a single temporary directory for all processing for this run
357
+ # This directory will be cleaned up at the end.
358
+ # The bootstrap script will be created inside this if run_unirig_command doesn't make its own.
359
+ # For clarity, run_unirig_command now handles its own bootstrap temp file.
360
  processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
361
  print(f"Using temporary processing directory: {processing_temp_dir}")
362
+
363
+ diagnostic_script_path = None # For cleanup in finally
364
+
365
  try:
366
  base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
 
367
  abs_input_glb_path = os.path.abspath(input_glb_path)
368
  abs_skeleton_output_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
369
  abs_skin_output_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
370
  abs_final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
371
+ unirig_script_to_run = UNIRIG_RUN_PY # This is an absolute path
372
 
373
+ print("\n--- Running Blender Python Environment Diagnostic Test (via bootstrap) ---")
374
+ # Content of the diagnostic script remains the same
375
  diagnostic_script_content = f"""
376
  import sys
377
  import os
 
385
  print(os.environ.get('PYTHONPATH', 'PYTHONPATH not set or empty'))
386
  print("\\nLD_LIBRARY_PATH Environment Variable (as seen by script):")
387
  print(os.environ.get('LD_LIBRARY_PATH', 'LD_LIBRARY_PATH not set or empty'))
 
 
388
  print("\\n--- Attempting Imports ---")
389
  try:
390
  import bpy
391
  print("SUCCESS: 'bpy' imported.")
 
 
 
392
  except Exception as e:
393
+ print(f"FAILED to import 'bpy': {{e}}")
394
  traceback.print_exc()
395
  try:
396
  print("\\nChecking for 'src' in CWD (should be UniRig repo root):")
397
+ if os.path.isdir('src'): # This check is relative to CWD
398
  print(" 'src' directory FOUND in CWD.")
399
  if os.path.isfile(os.path.join('src', '__init__.py')):
400
  print(" 'src/__init__.py' FOUND.")
401
  else:
402
+ print(" WARNING: 'src/__init__.py' NOT FOUND.")
403
  else:
404
  print(" 'src' directory NOT FOUND in CWD.")
405
  print("\\nAttempting: from src.inference.download import download")
406
+ from src.inference.download import download
407
  print("SUCCESS: 'from src.inference.download import download' worked.")
408
  except ImportError as e:
409
  print(f"FAILED: 'from src.inference.download import download': {{e}}")
 
413
  traceback.print_exc()
414
  print("--- End Diagnostic Info ---")
415
  """
416
+ # Save diagnostic script to the processing_temp_dir
417
  diagnostic_script_path = os.path.join(processing_temp_dir, "env_diagnostic_test.py")
418
  with open(diagnostic_script_path, "w") as f: f.write(diagnostic_script_content)
419
+
420
+ run_unirig_command(diagnostic_script_path, [], "Blender Env Diagnostic") # Args are empty for diagnostic
421
+ print("--- Finished Blender Python Environment Diagnostic Test ---\n")
422
+ # If the above didn't raise an error, sys.path was likely fixed by bootstrap for the diagnostic.
 
 
 
 
 
423
 
424
  unirig_device_arg = "device=cpu"
425
  if DEVICE.type == 'cuda':
 
468
  return gr.update(value=abs_final_rigged_glb_path)
469
  except gr.Error as e:
470
  print(f"A Gradio Error occurred: {e}")
471
+ # No need to re-raise, run_unirig_command already does or it's an explicit raise here.
472
+ # Let Gradio handle displaying it.
473
+ raise # Re-raise to ensure Gradio UI shows the error
474
  except Exception as e:
475
  print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
476
  import traceback; traceback.print_exc()
477
+ raise gr.Error(f"An unexpected error occurred: {str(e)[:500]}. Check logs.")
 
478
  finally:
479
+ # Cleanup diagnostic script if it was created
480
+ if diagnostic_script_path and os.path.exists(diagnostic_script_path):
481
+ try:
482
+ os.remove(diagnostic_script_path)
483
+ print(f"Cleaned up diagnostic script: {diagnostic_script_path}")
484
+ except Exception as cleanup_e:
485
+ print(f"Error cleaning up diagnostic script {diagnostic_script_path}: {cleanup_e}")
486
+ # Cleanup the main processing directory
487
  if os.path.exists(processing_temp_dir):
488
  try:
489
  shutil.rmtree(processing_temp_dir)
 
500
 
501
  startup_error_message = None
502
  if not blender_executable_to_use:
503
+ startup_error_message = (f"CRITICAL STARTUP ERROR: Blender executable not located. Expected at {BLENDER_EXEC}")
504
  elif not unirig_repo_ok:
505
  startup_error_message = (f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}.")
506
  elif not unirig_run_py_ok:
 
509
  if startup_error_message:
510
  print(startup_error_message)
511
  with gr.Blocks(theme=theme) as iface:
512
+ gr.Markdown(f"# Application Startup Error\n\n{startup_error_message}\n\nPlease check Space logs.")
513
  else:
514
  with gr.Blocks(theme=theme) as iface:
515
  gr.Markdown(
516
  f"""
517
  # UniRig Auto-Rigger (Blender {BLENDER_PYTHON_VERSION_DIR} / Python {BLENDER_PYTHON_VERSION})
518
+ Upload a `.glb` mesh. UniRig predicts skeleton and skinning weights via Blender's Python.
519
+ * App Python: `{sys.version.split()[0]}`, UniRig (Blender): `{BLENDER_PYTHON_VERSION}`.
520
+ * Device: **{DEVICE.type.upper()}**.
521
+ * Blender: `{blender_executable_to_use}`.
522
  * UniRig Source: [https://github.com/VAST-AI-Research/UniRig](https://github.com/VAST-AI-Research/UniRig)
523
  """
524
  )
 
526
  with gr.Column(scale=1):
527
  input_model = gr.File(
528
  label="Upload .glb Mesh File",
529
+ type="filepath",
530
  file_types=[".glb"]
531
  )
532
  submit_button = gr.Button("Rig Model", variant="primary")
 
546
  print("Launching Gradio interface...")
547
  iface.launch(share=False, ssr_mode=False)
548
  else:
549
+ print("ERROR: Gradio interface not created due to startup errors. Check logs.")
550