jkorstad commited on
Commit
fd9d8fc
·
verified ·
1 Parent(s): 432a66c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +151 -146
app.py CHANGED
@@ -5,7 +5,6 @@ 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
 
@@ -14,172 +13,178 @@ import spaces
14
  UNIRIG_REPO_DIR = os.path.join(os.path.dirname(__file__), "UniRig")
15
 
16
  if not os.path.isdir(UNIRIG_REPO_DIR):
17
- print(f"ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
18
- # Consider raising an error or displaying it in the UI if UniRig is critical for startup
19
 
20
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
21
  print(f"Using device: {DEVICE}")
22
  if DEVICE.type == 'cuda':
23
- print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
24
- print(f"CUDA Version: {torch.version.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
- # It's generally better to ensure UniRig's internal scripts handle PYTHONPATH if needed,
39
- # or that it's installed in a way that `python -m` works correctly from its root.
40
- # Setting cwd=UNIRIG_REPO_DIR is often the key for Hydra.
41
-
42
- try:
43
- # Execute the command from the UniRig directory for Hydra to find configs
44
- result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
45
- print(f"{step_name} STDOUT:\n{result.stdout}")
46
- if result.stderr:
47
- print(f"{step_name} STDERR (non-fatal or warnings):\n{result.stderr}")
48
- except subprocess.CalledProcessError as e:
49
- print(f"ERROR during {step_name}:")
50
- print(f"Command: {' '.join(e.cmd)}")
51
- print(f"Return code: {e.returncode}")
52
- print(f"Stdout: {e.stdout}")
53
- print(f"Stderr: {e.stderr}")
54
- # Provide a more user-friendly error, potentially masking long tracebacks
55
- error_summary = e.stderr.splitlines()[-5:] # Last 5 lines of stderr
56
- raise gr.Error(f"Error in UniRig {step_name}. Details: {' '.join(error_summary)}")
57
- except FileNotFoundError:
58
- print(f"ERROR: Could not find executable or script for {step_name}. Is UniRig cloned correctly in {UNIRIG_REPO_DIR} and Python environment setup?")
59
- raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs and UniRig directory structure.")
60
- except Exception as e_general:
61
- print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
62
- raise gr.Error(f"Unexpected Python error during {step_name}: {str(e_general)[:500]}")
63
-
64
-
 
 
 
 
 
 
 
65
  # @spaces.GPU # You can specify type like @spaces.GPU(type="t4") or count
66
- @spaces.GPU
67
  def rig_glb_mesh_multistep(input_glb_file_obj):
68
- """
69
- Takes an input GLB file object (from gr.File with type="filepath"),
70
- rigs it using the new UniRig multi-step process,
71
- and returns the path to the final rigged GLB file.
72
- """
73
- if not os.path.isdir(UNIRIG_REPO_DIR):
74
- raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Cannot proceed. Please check Space setup.")
75
-
76
- if input_glb_file_obj is None:
77
- # This case should ideally be handled by Gradio's input validation if `allow_none=False` (default)
78
- raise gr.Error("No input file provided. Please upload a .glb mesh.")
79
-
80
- # When type="filepath", input_glb_file_obj is the path string directly
81
- input_glb_path = input_glb_file_obj
82
- print(f"Input GLB path received: {input_glb_path}")
83
-
84
- # Create a dedicated temporary directory for all intermediate and final files
85
- processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
86
- print(f"Using temporary processing directory: {processing_temp_dir}")
87
-
88
- try:
89
- base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
90
-
91
- # Step 1: Skeleton Prediction
92
- temp_skeleton_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
93
- print("Step 1: Predicting Skeleton...")
94
- run_unirig_command([
95
- "unirig.predict_skeleton",
96
- f"input.path={os.path.abspath(input_glb_path)}", # Use absolute path for robustness
97
- f"output.path={os.path.abspath(temp_skeleton_path)}",
98
- # f"device={str(DEVICE)}" # If UniRig's script accepts this override and handles it
99
- ], "Skeleton Prediction")
100
- if not os.path.exists(temp_skeleton_path):
101
- raise gr.Error("Skeleton prediction failed to produce an output file. Check logs for UniRig errors.")
102
-
103
- # Step 2: Skinning Weight Prediction
104
- temp_skin_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
105
- print("Step 2: Predicting Skinning Weights...")
106
- run_unirig_command([
107
- "unirig.predict_skin",
108
- f"input.skeleton_path={os.path.abspath(temp_skeleton_path)}",
109
- f"input.source_mesh_path={os.path.abspath(input_glb_path)}",
110
- f"output.path={os.path.abspath(temp_skin_path)}",
111
- ], "Skinning Prediction")
112
- if not os.path.exists(temp_skin_path):
113
- raise gr.Error("Skinning prediction failed to produce an output file. Check logs for UniRig errors.")
114
-
115
- # Step 3: Merge Skeleton/Skin with Original Mesh
116
- final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
117
- print("Step 3: Merging Results...")
118
- run_unirig_command([
119
- "unirig.merge_skeleton_skin",
120
- f"input.source_rig_path={os.path.abspath(temp_skin_path)}",
121
- f"input.target_mesh_path={os.path.abspath(input_glb_path)}",
122
- f"output.path={os.path.abspath(final_rigged_glb_path)}",
123
- ], "Merging")
124
- if not os.path.exists(final_rigged_glb_path):
125
- raise gr.Error("Merging process failed to produce the final rigged GLB file. Check logs for UniRig errors.")
126
-
127
- # final_rigged_glb_path is in processing_temp_dir.
128
- # Gradio's gr.Model3D output component will handle serving this file.
129
- return final_rigged_glb_path
130
-
131
- except gr.Error: # Re-raise Gradio errors directly
132
- if os.path.exists(processing_temp_dir): # Clean up on known Gradio error
133
- shutil.rmtree(processing_temp_dir)
134
- print(f"Cleaned up temporary directory: {processing_temp_dir}")
135
- raise
136
- except Exception as e:
137
- print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
138
- if os.path.exists(processing_temp_dir): # Clean up on unexpected error
139
- shutil.rmtree(processing_temp_dir)
140
- print(f"Cleaned up temporary directory: {processing_temp_dir}")
141
- raise gr.Error(f"An unexpected error occurred during processing: {str(e)[:500]}")
142
 
143
 
144
  # --- Gradio Interface ---
145
  theme = gr.themes.Soft(
146
- primary_hue=gr.themes.colors.sky,
147
- secondary_hue=gr.themes.colors.blue,
148
- neutral_hue=gr.themes.colors.slate,
149
- font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
150
  )
151
 
152
  # Ensure UNIRIG_REPO_DIR check happens before interface is built if it's critical
153
  if not os.path.isdir(UNIRIG_REPO_DIR) and __name__ == "__main__": # Check only if running as main script
154
- print(f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will not work.")
155
 
156
  # Define the interface
157
  # Note: The @spaces.GPU decorator would go above the function `rig_glb_mesh_multistep`
158
  iface = gr.Interface(
159
- fn=rig_glb_mesh_multistep,
160
- inputs=gr.File(
161
- label="Upload .glb Mesh File",
162
- type="filepath" # Corrected type for Gradio 5.x.x
163
- ),
164
- outputs=gr.Model3D(
165
- label="Rigged 3D Model (.glb)",
166
- clear_color=[0.8, 0.8, 0.8, 1.0],
167
- ),
168
- title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
169
- description=(
170
- "Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
171
- "The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
172
- "This may take several minutes. Ensure your GLB has clean geometry.\n"
173
- f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{os.path.basename(UNIRIG_REPO_DIR)}'.\n"
174
- f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
175
- ),
176
- cache_examples=False,
177
- theme=theme,
178
- allow_flagging="never"
 
179
  )
180
 
181
  if __name__ == "__main__":
182
- if not os.path.isdir(UNIRIG_REPO_DIR):
183
- print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. Ensure it's cloned in the Space's root.")
184
-
185
- iface.launch()
 
5
  import tempfile
6
  import shutil
7
  import subprocess
 
8
  # from huggingface_hub import HfApi, snapshot_download # For future model management if needed
9
  # import spaces # For @spaces.GPU decorator if you add it
10
 
 
13
  UNIRIG_REPO_DIR = os.path.join(os.path.dirname(__file__), "UniRig")
14
 
15
  if not os.path.isdir(UNIRIG_REPO_DIR):
16
+ print(f"ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
17
+ # Consider raising an error or displaying it in the UI if UniRig is critical for startup
18
 
19
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
20
  print(f"Using device: {DEVICE}")
21
  if DEVICE.type == 'cuda':
22
+ print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
23
+ print(f"CUDA Version: {torch.version.cuda}")
24
  else:
25
+ print("Warning: CUDA not available or not detected by PyTorch. UniRig performance will be severely impacted.")
26
 
 
27
  def run_unirig_command(command_args, step_name):
28
+ """Helper function to run UniRig commands using subprocess."""
29
+ python_exe = sys.executable
30
+ # Ensure the command starts with the python executable and '-m' for module execution
31
+ cmd = [python_exe, "-m"] + command_args
32
+
33
+ print(f"Running {step_name}: {' '.join(cmd)}")
34
+
35
+ process_env = os.environ.copy()
36
+
37
+ # Explicitly add UNIRIG_REPO_DIR to PYTHONPATH for the subprocess.
38
+ # This ensures that Python can find the 'unirig' package located within UNIRIG_REPO_DIR.
39
+ # UNIRIG_REPO_DIR itself is the directory containing the 'unirig' package folder.
40
+ existing_pythonpath = process_env.get('PYTHONPATH', '')
41
+ process_env["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{existing_pythonpath}"
42
+ print(f"Set PYTHONPATH for subprocess: {process_env['PYTHONPATH']}")
43
+
44
+
45
+ try:
46
+ # Execute the command from the UniRig directory for Hydra to find configs
47
+ result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
48
+ print(f"{step_name} STDOUT:\n{result.stdout}")
49
+ if result.stderr:
50
+ print(f"{step_name} STDERR (non-fatal or warnings):\n{result.stderr}")
51
+ except subprocess.CalledProcessError as e:
52
+ print(f"ERROR during {step_name}:")
53
+ print(f"Command: {' '.join(e.cmd)}")
54
+ print(f"Return code: {e.returncode}")
55
+ print(f"Stdout: {e.stdout}")
56
+ print(f"Stderr: {e.stderr}")
57
+ # Provide a more user-friendly error, potentially masking long tracebacks
58
+ error_summary = e.stderr.splitlines()[-5:] # Last 5 lines of stderr
59
+ raise gr.Error(f"Error in UniRig {step_name}. Details: {' '.join(error_summary)}")
60
+ except FileNotFoundError:
61
+ print(f"ERROR: Could not find executable or script for {step_name}. Is UniRig cloned correctly in {UNIRIG_REPO_DIR} and Python environment setup?")
62
+ raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs and UniRig directory structure.")
63
+ except Exception as e_general:
64
+ print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
65
+ raise gr.Error(f"Unexpected Python error during {step_name}: {str(e_general)[:500]}")
66
+
67
+
68
+ # If you are using @spaces.GPU, you would import it:
69
+ # import spaces
70
  # @spaces.GPU # You can specify type like @spaces.GPU(type="t4") or count
 
71
  def rig_glb_mesh_multistep(input_glb_file_obj):
72
+ """
73
+ Takes an input GLB file object (from gr.File with type="filepath"),
74
+ rigs it using the new UniRig multi-step process,
75
+ and returns the path to the final rigged GLB file.
76
+ """
77
+ if not os.path.isdir(UNIRIG_REPO_DIR):
78
+ raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Cannot proceed. Please check Space setup.")
79
+
80
+ if input_glb_file_obj is None:
81
+ # This case should ideally be handled by Gradio's input validation if `allow_none=False` (default)
82
+ raise gr.Error("No input file provided. Please upload a .glb mesh.")
83
+
84
+ # When type="filepath", input_glb_file_obj is the path string directly
85
+ input_glb_path = input_glb_file_obj
86
+ print(f"Input GLB path received: {input_glb_path}")
87
+
88
+ # Create a dedicated temporary directory for all intermediate and final files
89
+ processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
90
+ print(f"Using temporary processing directory: {processing_temp_dir}")
91
+
92
+ try:
93
+ base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
94
+
95
+ # Step 1: Skeleton Prediction
96
+ temp_skeleton_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
97
+ print("Step 1: Predicting Skeleton...")
98
+ run_unirig_command([
99
+ "unirig.predict_skeleton",
100
+ f"input.path={os.path.abspath(input_glb_path)}", # Use absolute path for robustness
101
+ f"output.path={os.path.abspath(temp_skeleton_path)}",
102
+ # f"device={str(DEVICE)}" # If UniRig's script accepts this override and handles it
103
+ ], "Skeleton Prediction")
104
+ if not os.path.exists(temp_skeleton_path):
105
+ raise gr.Error("Skeleton prediction failed to produce an output file. Check logs for UniRig errors.")
106
+
107
+ # Step 2: Skinning Weight Prediction
108
+ temp_skin_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
109
+ print("Step 2: Predicting Skinning Weights...")
110
+ run_unirig_command([
111
+ "unirig.predict_skin",
112
+ f"input.skeleton_path={os.path.abspath(temp_skeleton_path)}",
113
+ f"input.source_mesh_path={os.path.abspath(input_glb_path)}",
114
+ f"output.path={os.path.abspath(temp_skin_path)}",
115
+ ], "Skinning Prediction")
116
+ if not os.path.exists(temp_skin_path):
117
+ raise gr.Error("Skinning prediction failed to produce an output file. Check logs for UniRig errors.")
118
+
119
+ # Step 3: Merge Skeleton/Skin with Original Mesh
120
+ final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
121
+ print("Step 3: Merging Results...")
122
+ run_unirig_command([
123
+ "unirig.merge_skeleton_skin",
124
+ f"input.source_rig_path={os.path.abspath(temp_skin_path)}",
125
+ f"input.target_mesh_path={os.path.abspath(input_glb_path)}",
126
+ f"output.path={os.path.abspath(final_rigged_glb_path)}",
127
+ ], "Merging")
128
+ if not os.path.exists(final_rigged_glb_path):
129
+ raise gr.Error("Merging process failed to produce the final rigged GLB file. Check logs for UniRig errors.")
130
+
131
+ # final_rigged_glb_path is in processing_temp_dir.
132
+ # Gradio's gr.Model3D output component will handle serving this file.
133
+ return final_rigged_glb_path
134
+
135
+ except gr.Error: # Re-raise Gradio errors directly
136
+ if os.path.exists(processing_temp_dir): # Clean up on known Gradio error
137
+ shutil.rmtree(processing_temp_dir)
138
+ print(f"Cleaned up temporary directory: {processing_temp_dir}")
139
+ raise
140
+ except Exception as e:
141
+ print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
142
+ if os.path.exists(processing_temp_dir): # Clean up on unexpected error
143
+ shutil.rmtree(processing_temp_dir)
144
+ print(f"Cleaned up temporary directory: {processing_temp_dir}")
145
+ raise gr.Error(f"An unexpected error occurred during processing: {str(e)[:500]}")
146
 
147
 
148
  # --- Gradio Interface ---
149
  theme = gr.themes.Soft(
150
+ primary_hue=gr.themes.colors.sky,
151
+ secondary_hue=gr.themes.colors.blue,
152
+ neutral_hue=gr.themes.colors.slate,
153
+ font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
154
  )
155
 
156
  # Ensure UNIRIG_REPO_DIR check happens before interface is built if it's critical
157
  if not os.path.isdir(UNIRIG_REPO_DIR) and __name__ == "__main__": # Check only if running as main script
158
+ print(f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will not work.")
159
 
160
  # Define the interface
161
  # Note: The @spaces.GPU decorator would go above the function `rig_glb_mesh_multistep`
162
  iface = gr.Interface(
163
+ fn=rig_glb_mesh_multistep,
164
+ inputs=gr.File(
165
+ label="Upload .glb Mesh File",
166
+ type="filepath" # Corrected type for Gradio 4.x / 5.x
167
+ ),
168
+ outputs=gr.Model3D(
169
+ label="Rigged 3D Model (.glb)",
170
+ clear_color=[0.8, 0.8, 0.8, 1.0],
171
+ ),
172
+ title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
173
+ description=(
174
+ "Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
175
+ "The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
176
+ "This may take several minutes. Ensure your GLB has clean geometry.\n"
177
+ f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{os.path.basename(UNIRIG_REPO_DIR)}'.\n"
178
+ f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
179
+ ),
180
+ cache_examples=False,
181
+ theme=theme
182
+ # allow_flagging="never" # Removed as it's deprecated in Gradio 4.x and default behavior is usually no flagging.
183
+ # If specific flagging control is needed, use `flagging_options` or similar.
184
  )
185
 
186
  if __name__ == "__main__":
187
+ if not os.path.isdir(UNIRIG_REPO_DIR):
188
+ print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. Ensure it's cloned in the Space's root.")
189
+
190
+ iface.launch()