jkorstad commited on
Commit
e7da273
·
verified ·
1 Parent(s): e060cc0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -64
app.py CHANGED
@@ -5,9 +5,7 @@ import sys
5
  import tempfile
6
  import shutil
7
  import subprocess
8
- import spaces
9
- # from huggingface_hub import HfApi, snapshot_download # For future model management if needed
10
- # import spaces # For @spaces.GPU decorator if you add it
11
 
12
  # --- Configuration ---
13
  # Path to the cloned UniRig repository directory within the Space
@@ -25,27 +23,40 @@ if DEVICE.type == 'cuda':
25
  else:
26
  print("Warning: CUDA not available or not detected by PyTorch. UniRig performance will be severely impacted.")
27
 
28
- @spaces.GPU
29
- def run_unirig_command(command_args, step_name):
30
- """Helper function to run UniRig commands using subprocess."""
31
- python_exe = sys.executable
32
- # Ensure the command starts with the python executable and '-m' for module execution
33
- cmd = [python_exe, "-m"] + command_args
 
 
34
 
35
  print(f"Running {step_name}: {' '.join(cmd)}")
36
 
37
  process_env = os.environ.copy()
38
 
39
- # Explicitly add UNIRIG_REPO_DIR to PYTHONPATH for the subprocess.
40
- # This ensures that Python can find the 'unirig' package located within UNIRIG_REPO_DIR.
41
- # UNIRIG_REPO_DIR itself is the directory containing the 'unirig' package folder.
 
 
 
 
42
  existing_pythonpath = process_env.get('PYTHONPATH', '')
43
- process_env["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{existing_pythonpath}"
 
 
 
 
 
44
  print(f"Set PYTHONPATH for subprocess: {process_env['PYTHONPATH']}")
45
 
46
 
47
  try:
48
- # Execute the command from the UniRig directory for Hydra to find configs
 
 
49
  result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
50
  print(f"{step_name} STDOUT:\n{result.stdout}")
51
  if result.stderr:
@@ -60,87 +71,92 @@ def run_unirig_command(command_args, step_name):
60
  error_summary = e.stderr.splitlines()[-5:] # Last 5 lines of stderr
61
  raise gr.Error(f"Error in UniRig {step_name}. Details: {' '.join(error_summary)}")
62
  except FileNotFoundError:
63
- print(f"ERROR: Could not find executable or script for {step_name}. Is UniRig cloned correctly in {UNIRIG_REPO_DIR} and Python environment setup?")
64
- raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs and UniRig directory structure.")
 
65
  except Exception as e_general:
66
  print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
67
  raise gr.Error(f"Unexpected Python error during {step_name}: {str(e_general)[:500]}")
68
 
69
 
70
- # @spaces.GPU # You can specify type like @spaces.GPU(type="t4") or count
71
- @spaces.GPU
72
  def rig_glb_mesh_multistep(input_glb_file_obj):
73
  """
74
  Takes an input GLB file object (from gr.File with type="filepath"),
75
- rigs it using the new UniRig multi-step process,
76
  and returns the path to the final rigged GLB file.
77
  """
78
  if not os.path.isdir(UNIRIG_REPO_DIR):
79
  raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Cannot proceed. Please check Space setup.")
80
 
81
  if input_glb_file_obj is None:
82
- # This case should ideally be handled by Gradio's input validation if `allow_none=False` (default)
83
  raise gr.Error("No input file provided. Please upload a .glb mesh.")
84
 
85
- # When type="filepath", input_glb_file_obj is the path string directly
86
- input_glb_path = input_glb_file_obj
87
  print(f"Input GLB path received: {input_glb_path}")
88
 
89
  # Create a dedicated temporary directory for all intermediate and final files
 
90
  processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
91
  print(f"Using temporary processing directory: {processing_temp_dir}")
92
 
93
  try:
94
  base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
95
 
96
- # Step 1: Skeleton Prediction
97
- temp_skeleton_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
 
 
 
 
98
  print("Step 1: Predicting Skeleton...")
99
- run_unirig_command([
100
- "unirig.predict_skeleton",
101
- f"input.path={os.path.abspath(input_glb_path)}", # Use absolute path for robustness
102
- f"output.path={os.path.abspath(temp_skeleton_path)}",
103
- # f"device={str(DEVICE)}" # If UniRig's script accepts this override and handles it
104
- ], "Skeleton Prediction")
105
- if not os.path.exists(temp_skeleton_path):
106
  raise gr.Error("Skeleton prediction failed to produce an output file. Check logs for UniRig errors.")
107
 
108
- # Step 2: Skinning Weight Prediction
109
- temp_skin_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
110
  print("Step 2: Predicting Skinning Weights...")
111
- run_unirig_command([
112
- "unirig.predict_skin",
113
- f"input.skeleton_path={os.path.abspath(temp_skeleton_path)}",
114
- f"input.source_mesh_path={os.path.abspath(input_glb_path)}",
115
- f"output.path={os.path.abspath(temp_skin_path)}",
116
- ], "Skinning Prediction")
117
- if not os.path.exists(temp_skin_path):
 
 
 
118
  raise gr.Error("Skinning prediction failed to produce an output file. Check logs for UniRig errors.")
119
 
120
- # Step 3: Merge Skeleton/Skin with Original Mesh
121
- final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
122
  print("Step 3: Merging Results...")
123
- run_unirig_command([
124
- "unirig.merge_skeleton_skin",
125
- f"input.source_rig_path={os.path.abspath(temp_skin_path)}",
126
- f"input.target_mesh_path={os.path.abspath(input_glb_path)}",
127
- f"output.path={os.path.abspath(final_rigged_glb_path)}",
128
- ], "Merging")
129
- if not os.path.exists(final_rigged_glb_path):
 
 
 
130
  raise gr.Error("Merging process failed to produce the final rigged GLB file. Check logs for UniRig errors.")
131
 
132
- # final_rigged_glb_path is in processing_temp_dir.
133
- # Gradio's gr.Model3D output component will handle serving this file.
134
- return final_rigged_glb_path
135
 
136
- except gr.Error: # Re-raise Gradio errors directly
137
- if os.path.exists(processing_temp_dir): # Clean up on known Gradio error
138
  shutil.rmtree(processing_temp_dir)
139
  print(f"Cleaned up temporary directory: {processing_temp_dir}")
140
  raise
141
  except Exception as e:
142
  print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
143
- if os.path.exists(processing_temp_dir): # Clean up on unexpected error
144
  shutil.rmtree(processing_temp_dir)
145
  print(f"Cleaned up temporary directory: {processing_temp_dir}")
146
  raise gr.Error(f"An unexpected error occurred during processing: {str(e)[:500]}")
@@ -154,17 +170,14 @@ theme = gr.themes.Soft(
154
  font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
155
  )
156
 
157
- # Ensure UNIRIG_REPO_DIR check happens before interface is built if it's critical
158
- if not os.path.isdir(UNIRIG_REPO_DIR) and __name__ == "__main__": # Check only if running as main script
159
  print(f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will not work.")
160
 
161
- # Define the interface
162
- # Note: The @spaces.GPU decorator would go above the function `rig_glb_mesh_multistep`
163
  iface = gr.Interface(
164
  fn=rig_glb_mesh_multistep,
165
  inputs=gr.File(
166
  label="Upload .glb Mesh File",
167
- type="filepath" # Corrected type for Gradio 5.x
168
  ),
169
  outputs=gr.Model3D(
170
  label="Rigged 3D Model (.glb)",
@@ -172,7 +185,7 @@ iface = gr.Interface(
172
  ),
173
  title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
174
  description=(
175
- "Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
176
  "The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
177
  "This may take several minutes. Ensure your GLB has clean geometry.\n"
178
  f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{os.path.basename(UNIRIG_REPO_DIR)}'.\n"
@@ -180,12 +193,10 @@ iface = gr.Interface(
180
  ),
181
  cache_examples=False,
182
  theme=theme
183
- # allow_flagging="never" # Removed as it's deprecated in Gradio 4.x and default behavior is usually no flagging.
184
- # If specific flagging control is needed, use `flagging_options` or similar.
185
  )
186
 
187
  if __name__ == "__main__":
188
  if not os.path.isdir(UNIRIG_REPO_DIR):
189
  print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. Ensure it's cloned in the Space's root.")
190
 
191
- iface.launch()
 
5
  import tempfile
6
  import shutil
7
  import subprocess
8
+ import spaces # Ensure spaces is imported if @spaces.GPU is used
 
 
9
 
10
  # --- Configuration ---
11
  # Path to the cloned UniRig repository directory within the Space
 
23
  else:
24
  print("Warning: CUDA not available or not detected by PyTorch. UniRig performance will be severely impacted.")
25
 
26
+ @spaces.GPU # Decorator for ZeroGPU
27
+ def run_unirig_command(command_list, step_name):
28
+ """
29
+ Helper function to run UniRig commands (now expecting bash scripts) using subprocess.
30
+ command_list: The full command and its arguments, e.g., ["bash", "script.sh", "--arg", "value"]
31
+ """
32
+ # The command_list is now expected to be the full command, e.g., starting with "bash"
33
+ cmd = command_list
34
 
35
  print(f"Running {step_name}: {' '.join(cmd)}")
36
 
37
  process_env = os.environ.copy()
38
 
39
+ # Determine the path to the 'src' directory within UniRig, where the 'unirig' package resides.
40
+ unirig_src_dir = os.path.join(UNIRIG_REPO_DIR, "src")
41
+
42
+ # Explicitly add UNIRIG_REPO_DIR/src to PYTHONPATH for the subprocess.
43
+ # The bash scripts will internally call Python, which needs to find the 'unirig' package.
44
+ # Also, keep UNIRIG_REPO_DIR itself in case some scripts or modules there are run directly
45
+ # or expect the project root to be in PYTHONPATH.
46
  existing_pythonpath = process_env.get('PYTHONPATH', '')
47
+ new_pythonpath_parts = [unirig_src_dir, UNIRIG_REPO_DIR] # UniRig/src first, then UniRig/
48
+ if existing_pythonpath:
49
+ # Prepend our paths to existing PYTHONPATH
50
+ new_pythonpath_parts.extend(existing_pythonpath.split(os.pathsep))
51
+
52
+ process_env["PYTHONPATH"] = os.pathsep.join(filter(None, new_pythonpath_parts)) # filter(None,...) handles empty existing_pythonpath
53
  print(f"Set PYTHONPATH for subprocess: {process_env['PYTHONPATH']}")
54
 
55
 
56
  try:
57
+ # Execute the command from the UniRig directory (UNIRIG_REPO_DIR)
58
+ # This is crucial for the bash scripts to find their relative paths (e.g., to Python scripts)
59
+ # and for any underlying Python/Hydra calls to find configurations (e.g., in UniRig/configs/)
60
  result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
61
  print(f"{step_name} STDOUT:\n{result.stdout}")
62
  if result.stderr:
 
71
  error_summary = e.stderr.splitlines()[-5:] # Last 5 lines of stderr
72
  raise gr.Error(f"Error in UniRig {step_name}. Details: {' '.join(error_summary)}")
73
  except FileNotFoundError:
74
+ # This error means the executable (e.g., "bash" or the script itself) was not found.
75
+ print(f"ERROR: Could not find executable or script for {step_name}. Command: {' '.join(cmd)}. Is UniRig cloned correctly and 'bash' available?")
76
+ raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs, UniRig directory structure, and script paths.")
77
  except Exception as e_general:
78
  print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
79
  raise gr.Error(f"Unexpected Python error during {step_name}: {str(e_general)[:500]}")
80
 
81
 
82
+ @spaces.GPU # Decorator for ZeroGPU
 
83
  def rig_glb_mesh_multistep(input_glb_file_obj):
84
  """
85
  Takes an input GLB file object (from gr.File with type="filepath"),
86
+ rigs it using the new UniRig multi-step process by calling its bash scripts,
87
  and returns the path to the final rigged GLB file.
88
  """
89
  if not os.path.isdir(UNIRIG_REPO_DIR):
90
  raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Cannot proceed. Please check Space setup.")
91
 
92
  if input_glb_file_obj is None:
 
93
  raise gr.Error("No input file provided. Please upload a .glb mesh.")
94
 
95
+ input_glb_path = input_glb_file_obj # This is the absolute path from gr.File(type="filepath")
 
96
  print(f"Input GLB path received: {input_glb_path}")
97
 
98
  # Create a dedicated temporary directory for all intermediate and final files
99
+ # The output paths for UniRig scripts will point into this directory.
100
  processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
101
  print(f"Using temporary processing directory: {processing_temp_dir}")
102
 
103
  try:
104
  base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
105
 
106
+ # Define absolute paths for intermediate files within the processing_temp_dir
107
+ abs_skeleton_output_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
108
+ abs_skin_output_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
109
+ abs_final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
110
+
111
+ # Step 1: Skeleton Prediction using generate_skeleton.sh
112
  print("Step 1: Predicting Skeleton...")
113
+ skeleton_cmd = [
114
+ "bash", "launch/inference/generate_skeleton.sh",
115
+ "--input", input_glb_path, # Input is the original GLB
116
+ "--output", abs_skeleton_output_path
117
+ ]
118
+ run_unirig_command(skeleton_cmd, "Skeleton Prediction")
119
+ if not os.path.exists(abs_skeleton_output_path):
120
  raise gr.Error("Skeleton prediction failed to produce an output file. Check logs for UniRig errors.")
121
 
122
+ # Step 2: Skinning Weight Prediction using generate_skin.sh
 
123
  print("Step 2: Predicting Skinning Weights...")
124
+ # generate_skin.sh requires the skeleton from step 1 as --input,
125
+ # and the original mesh as --source.
126
+ skin_cmd = [
127
+ "bash", "launch/inference/generate_skin.sh",
128
+ "--input", abs_skeleton_output_path, # Input is the skeleton FBX from previous step
129
+ "--source", input_glb_path, # Source is the original GLB mesh
130
+ "--output", abs_skin_output_path
131
+ ]
132
+ run_unirig_command(skin_cmd, "Skinning Prediction")
133
+ if not os.path.exists(abs_skin_output_path):
134
  raise gr.Error("Skinning prediction failed to produce an output file. Check logs for UniRig errors.")
135
 
136
+ # Step 3: Merge Skeleton/Skin with Original Mesh using merge.sh
 
137
  print("Step 3: Merging Results...")
138
+ # merge.sh requires the skinned FBX as --source (which contains skeleton and weights)
139
+ # and the original GLB as --target.
140
+ merge_cmd = [
141
+ "bash", "launch/inference/merge.sh",
142
+ "--source", abs_skin_output_path, # Source is the skinned FBX from previous step
143
+ "--target", input_glb_path, # Target is the original GLB mesh
144
+ "--output", abs_final_rigged_glb_path
145
+ ]
146
+ run_unirig_command(merge_cmd, "Merging")
147
+ if not os.path.exists(abs_final_rigged_glb_path):
148
  raise gr.Error("Merging process failed to produce the final rigged GLB file. Check logs for UniRig errors.")
149
 
150
+ return abs_final_rigged_glb_path
 
 
151
 
152
+ except gr.Error:
153
+ if os.path.exists(processing_temp_dir):
154
  shutil.rmtree(processing_temp_dir)
155
  print(f"Cleaned up temporary directory: {processing_temp_dir}")
156
  raise
157
  except Exception as e:
158
  print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
159
+ if os.path.exists(processing_temp_dir):
160
  shutil.rmtree(processing_temp_dir)
161
  print(f"Cleaned up temporary directory: {processing_temp_dir}")
162
  raise gr.Error(f"An unexpected error occurred during processing: {str(e)[:500]}")
 
170
  font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
171
  )
172
 
173
+ if not os.path.isdir(UNIRIG_REPO_DIR) and __name__ == "__main__":
 
174
  print(f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will not work.")
175
 
 
 
176
  iface = gr.Interface(
177
  fn=rig_glb_mesh_multistep,
178
  inputs=gr.File(
179
  label="Upload .glb Mesh File",
180
+ type="filepath"
181
  ),
182
  outputs=gr.Model3D(
183
  label="Rigged 3D Model (.glb)",
 
185
  ),
186
  title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
187
  description=(
188
+ "Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh by calling its provided bash scripts.\n"
189
  "The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
190
  "This may take several minutes. Ensure your GLB has clean geometry.\n"
191
  f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{os.path.basename(UNIRIG_REPO_DIR)}'.\n"
 
193
  ),
194
  cache_examples=False,
195
  theme=theme
 
 
196
  )
197
 
198
  if __name__ == "__main__":
199
  if not os.path.isdir(UNIRIG_REPO_DIR):
200
  print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. Ensure it's cloned in the Space's root.")
201
 
202
+ iface.launch()