jkorstad commited on
Commit
3561fbf
·
unverified ·
1 Parent(s): 476fd71

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -127
app.py CHANGED
@@ -1,172 +1,202 @@
1
  import gradio as gr
2
  import torch
3
- import trimesh
4
  import os
5
  import sys
6
  import tempfile
7
  import shutil
8
-
9
- # Add UniRig source directory to Python path
10
- # Assuming UniRig files are in a subdirectory named 'UniRig_src'
11
- sys.path.append(os.path.join(os.path.dirname(__file__), 'UniRig_src'))
12
-
13
- # Conditional import for AutoRigger and setup_source_mesh
14
- # This helps in providing a clearer error if UniRig_src is not found
15
- try:
16
- from autorig import AutoRigger
17
- from utils import setup_source_mesh
18
- except ImportError as e:
19
- print("Error importing from UniRig_src. Make sure the UniRig source files are in the 'UniRig_src' directory.")
20
- print(f"Details: {e}")
21
- # Define dummy functions if import fails, so Gradio can still load with an error message
22
- def AutoRigger(*args, **kwargs):
23
- raise RuntimeError("UniRig AutoRigger could not be loaded. Check UniRig_src setup.")
24
- def setup_source_mesh(mesh, *args, **kwargs):
25
- raise RuntimeError("UniRig setup_source_mesh could not be loaded. Check UniRig_src setup.")
26
 
27
  # --- Configuration ---
28
- # Define paths to the UniRig model files
29
- # These files should be placed in the 'model_files' directory in your Hugging Face Space
30
- MODEL_DIR = os.path.join(os.path.dirname(__file__), "model_files")
31
- SMPL_SKELETON_PATH = os.path.join(MODEL_DIR, "smpl_skeleton.pkl")
32
- SKIN_KPS_PREDICTOR_PATH = os.path.join(MODEL_DIR, "skin_kps_predictor.pkl")
 
 
 
 
 
33
 
34
- # Check if model files exist
35
- if not os.path.exists(SMPL_SKELETON_PATH) or not os.path.exists(SKIN_KPS_PREDICTOR_PATH):
36
- print(f"Warning: Model files not found at {MODEL_DIR}. Please ensure smpl_skeleton.pkl and skin_kps_predictor.pkl are present.")
37
 
38
  # Determine processing device (CUDA if available, otherwise CPU)
39
- # ZeroGPU on Hugging Face Spaces should provide CUDA
40
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
41
  print(f"Using device: {DEVICE}")
42
  if DEVICE.type == 'cuda':
43
  print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
44
  print(f"CUDA Version: {torch.version.cuda}")
 
45
  else:
46
- print("CUDA not available, UniRig performance will be significantly slower on CPU.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
 
49
  # --- Core Rigging Function ---
50
- def rig_glb_mesh(input_glb_file):
51
  """
52
- Takes an input GLB file, rigs it using UniRig, and returns the path to the rigged GLB file.
 
53
  """
54
- if input_glb_file is None:
 
 
 
55
  raise gr.Error("No input file provided. Please upload a .glb mesh.")
56
 
57
- input_glb_path = input_glb_file.name # Get the path of the uploaded file
58
 
59
- # Ensure UniRig components are loaded (they might be dummy if import failed)
60
- if not callable(getattr(AutoRigger, '__init__', None)) or not callable(setup_source_mesh):
61
- raise gr.Error("UniRig components are not correctly loaded. Please check the server logs and UniRig_src setup.")
 
62
 
63
  try:
64
- # Create a temporary directory for output
65
- temp_dir = tempfile.mkdtemp()
66
- output_glb_filename = "rigged_output.glb"
67
- output_glb_path = os.path.join(temp_dir, output_glb_filename)
68
-
69
- # 1. Load the mesh using trimesh
70
- print(f"Loading mesh from: {input_glb_path}")
71
- mesh = trimesh.load_mesh(input_glb_path, force='mesh', process=False)
72
-
73
- if not isinstance(mesh, trimesh.Trimesh):
74
- # If it's a Scene object, try to get a single geometry
75
- if isinstance(mesh, trimesh.Scene):
76
- if len(mesh.geometry) == 0:
77
- raise gr.Error("Input GLB file contains no mesh geometry.")
78
- # Concatenate all meshes in the scene into a single mesh
79
- # This is a common approach, but might not be ideal for all GLB files
80
- print(f"Input is a scene with {len(mesh.geometry)} geometries. Attempting to merge.")
81
- mesh = trimesh.util.concatenate(list(mesh.geometry.values()))
82
- if not isinstance(mesh, trimesh.Trimesh):
83
- raise gr.Error(f"Could not extract a valid mesh from the GLB scene. Found type: {type(mesh)}")
84
- else:
85
- raise gr.Error(f"Failed to load a valid mesh from the input file. Loaded type: {type(mesh)}")
86
-
87
- print("Mesh loaded successfully.")
88
-
89
- # 2. Preprocess the mesh (as per UniRig's example)
90
- # This step is crucial for UniRig to work correctly.
91
- # It involves canonicalization and remeshing.
92
- print("Preprocessing mesh...")
93
- mesh = setup_source_mesh(mesh, device=DEVICE)
94
- print("Mesh preprocessing complete.")
95
-
96
- # 3. Initialize the AutoRigger
97
- # Ensure model files are accessible
98
- if not os.path.exists(SMPL_SKELETON_PATH) or not os.path.exists(SKIN_KPS_PREDICTOR_PATH):
99
- raise gr.Error(f"UniRig model files not found. Searched in {MODEL_DIR}. Please check your Space's file structure.")
100
 
101
- print("Initializing AutoRigger...")
102
- autorigger = AutoRigger(SMPL_SKELETON_PATH, SKIN_KPS_PREDICTOR_PATH, device=DEVICE)
103
- print("AutoRigger initialized.")
104
-
105
- # 4. Perform rigging
106
- print("Starting rigging process...")
107
- # The `rig` method might require specific verts, faces, and normals if not handled by `setup_source_mesh`
108
- # Assuming `setup_source_mesh` prepares it adequately.
109
- output_dict = autorigger.rig(mesh)
110
- print("Rigging process complete.")
111
-
112
- # 5. Extract the rigged mesh
113
- rigged_mesh = output_dict['rigged_mesh'] # This should be a trimesh.Trimesh object
114
- print("Rigged mesh extracted.")
115
-
116
- # 6. Export the rigged mesh to GLB format
117
- print(f"Exporting rigged mesh to: {output_glb_path}")
118
- rigged_mesh.export(output_glb_path)
119
- print("Export complete.")
120
-
121
- return output_glb_path
122
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  except Exception as e:
124
- print(f"Error during rigging: {e}")
125
- # Clean up temp dir in case of error
126
- if 'temp_dir' in locals() and os.path.exists(temp_dir):
127
- shutil.rmtree(temp_dir)
128
- # Re-raise as Gradio error to display to user
129
- raise gr.Error(f"An error occurred during processing: {str(e)}")
130
- # No finally block for shutil.rmtree(temp_dir) here,
131
- # because Gradio needs the file path to serve it.
132
- # Gradio handles cleanup of temporary files created by gr.File.
133
 
134
  # --- Gradio Interface ---
135
- # Define a custom theme (Blue and Charcoal Gray)
136
- # Using Soft theme with sky blue and slate gray
137
  theme = gr.themes.Soft(
138
- primary_hue=gr.themes.colors.sky, # A nice blue
139
- secondary_hue=gr.themes.colors.blue, # Can be same as primary or a complementary blue
140
- neutral_hue=gr.themes.colors.slate, # Charcoal gray
141
  font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
142
- ).set(
143
- # Further fine-tuning if needed
144
- # button_primary_background_fill="*primary_500",
145
- # button_primary_text_color="white",
146
  )
147
 
148
- # Interface definition
149
  iface = gr.Interface(
150
- fn=rig_glb_mesh,
151
- inputs=gr.File(label="Upload .glb Mesh File", type="file"), # 'file' gives a NamedTemporaryFile object
152
- outputs=gr.Model3D(label="Rigged 3D Model (.glb)", clear_color=[0.8, 0.8, 0.8, 1.0]), # Model3D can display .glb
153
- title="UniRig Auto-Rigger for 3D Meshes",
 
 
 
 
 
 
154
  description=(
155
- "Upload a 3D mesh in `.glb` format. This application uses UniRig to automatically rig the mesh.\n"
156
- "The process may take a few minutes, especially for complex meshes. Ensure your GLB has clean geometry.\n"
157
- f"Running on: {str(DEVICE).upper()}. Model files expected in '{MODEL_DIR}'.\n"
 
158
  f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
159
  ),
160
- examples=[
161
- # Add paths to example GLB files if you include them in your Space
162
- # e.g., [os.path.join(os.path.dirname(__file__), "examples/sample_mesh.glb")]
163
- ],
164
- cache_examples=False, # Set to True if you have static examples and want to pre-process them
165
  theme=theme,
166
  allow_flagging="never"
167
  )
168
 
169
  if __name__ == "__main__":
170
- if not os.path.exists(os.path.join(os.path.dirname(__file__), 'UniRig_src')):
171
- print("CRITICAL: 'UniRig_src' directory not found. Please ensure UniRig source files are correctly placed.")
 
 
 
 
 
 
172
  iface.launch()
 
1
  import gradio as gr
2
  import torch
 
3
  import os
4
  import sys
5
  import tempfile
6
  import shutil
7
+ import subprocess
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  # --- Configuration ---
10
+ # Path to the cloned UniRig repository directory within the Space
11
+ # IMPORTANT: You must clone the UniRig repository into this directory in your Hugging Face Space.
12
+ UNIRIG_REPO_DIR = os.path.join(os.path.dirname(__file__), "UniRig")
13
+
14
+ # Check if UniRig directory exists
15
+ if not os.path.isdir(UNIRIG_REPO_DIR):
16
+ # This message will appear in logs, Gradio app might fail to start fully.
17
+ print(f"ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
18
+ # Optionally, raise an error to make it more visible if the app starts
19
+ # raise RuntimeError(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
20
 
 
 
 
21
 
22
  # Determine processing device (CUDA if available, otherwise CPU)
 
23
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
24
  print(f"Using device: {DEVICE}")
25
  if DEVICE.type == 'cuda':
26
  print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
27
  print(f"CUDA Version: {torch.version.cuda}")
28
+ # Note: UniRig scripts might manage device internally or via Hydra configs.
29
  else:
30
+ print("Warning: CUDA not available or not detected by PyTorch. UniRig performance will be severely impacted.")
31
+
32
+ def run_unirig_command(command_args, step_name):
33
+ """Helper function to run UniRig commands using subprocess."""
34
+ python_exe = sys.executable # Use the current python interpreter
35
+ cmd = [python_exe, "-m"] + command_args
36
+
37
+ print(f"Running {step_name}: {' '.join(cmd)}")
38
+
39
+ # UniRig scripts often expect to be run from the root of the UniRig repository
40
+ # because they use Hydra and relative paths for configs (e.g., conf/config.yaml)
41
+ process_env = os.environ.copy()
42
+
43
+ # Add UniRig's parent directory to PYTHONPATH so `import unirig` works if needed,
44
+ # and the UniRig directory itself so its internal imports work.
45
+ # However, `python -m` typically handles package discovery well if CWD is correct.
46
+ # process_env["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{process_env.get('PYTHONPATH', '')}"
47
+
48
+ try:
49
+ # Execute the command
50
+ # It's crucial to set `cwd=UNIRIG_REPO_DIR` for Hydra to find its configs.
51
+ result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
52
+ print(f"{step_name} output:\n{result.stdout}")
53
+ if result.stderr:
54
+ print(f"{step_name} errors (non-fatal if check=True passed):\n{result.stderr}")
55
+ except subprocess.CalledProcessError as e:
56
+ print(f"ERROR during {step_name}:")
57
+ print(f"Command: {' '.join(e.cmd)}")
58
+ print(f"Return code: {e.returncode}")
59
+ print(f"Stdout: {e.stdout}")
60
+ print(f"Stderr: {e.stderr}")
61
+ raise gr.Error(f"Error in UniRig {step_name}: {e.stderr[:500]}") # Show first 500 chars of error
62
+ except FileNotFoundError:
63
+ # This can happen if UNIRIG_REPO_DIR is not populated correctly or python_exe is wrong
64
+ print(f"ERROR: Could not find executable or script for {step_name}. Is UniRig cloned correctly in {UNIRIG_REPO_DIR}?")
65
+ raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs.")
66
 
67
 
68
  # --- Core Rigging Function ---
69
+ def rig_glb_mesh_multistep(input_glb_file_obj):
70
  """
71
+ Takes an input GLB file object, rigs it using the new UniRig multi-step process,
72
+ and returns the path to the final rigged GLB file.
73
  """
74
+ if not os.path.isdir(UNIRIG_REPO_DIR):
75
+ raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Cannot proceed.")
76
+
77
+ if input_glb_file_obj is None:
78
  raise gr.Error("No input file provided. Please upload a .glb mesh.")
79
 
80
+ input_glb_path = input_glb_file_obj.name # Path to the temporary uploaded file
81
 
82
+ # Create a dedicated temporary directory for all intermediate and final files for this run
83
+ # This helps in organization and cleanup.
84
+ processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
85
+ print(f"Using temporary processing directory: {processing_temp_dir}")
86
 
87
  try:
88
+ # Define paths for intermediate and final files within the processing_temp_dir
89
+ # UniRig scripts expect output paths.
90
+ base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ # Step 1: Skeleton Prediction
93
+ # Output is typically an FBX file for the skeleton
94
+ temp_skeleton_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
95
+ print("Step 1: Predicting Skeleton...")
96
+ # Command: python -m unirig.predict_skeleton +input_path=<input_glb_path> +output_path=<temp_skeleton_path>
97
+ # Note: UniRig's scripts might have default output locations or require specific Hydra overrides.
98
+ # The `+` syntax is for Hydra overrides.
99
+ # Check UniRig's `conf/predict_skeleton.yaml` for default config values.
100
+ run_unirig_command([
101
+ "unirig.predict_skeleton",
102
+ f"input.path={input_glb_path}", # Use dot notation for Hydra parameters
103
+ f"output.path={temp_skeleton_path}",
104
+ # Add other necessary overrides, e.g., for device if not auto-detected well
105
+ # f"device={str(DEVICE)}" # If UniRig's script accepts this override
106
+ ], "Skeleton Prediction")
107
+ print(f"Skeleton predicted at: {temp_skeleton_path}")
108
+ if not os.path.exists(temp_skeleton_path):
109
+ raise gr.Error("Skeleton prediction failed to produce an output file.")
110
+
111
+ # Step 2: Skinning Weight Prediction
112
+ # Input: skeleton FBX and original GLB. Output: skinned FBX (or other format)
113
+ temp_skin_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
114
+ print("Step 2: Predicting Skinning Weights...")
115
+ # Command: python -m unirig.predict_skin +input_path=<temp_skeleton_path> +output_path=<temp_skin_path> +source_mesh_path=<input_glb_path>
116
+ run_unirig_command([
117
+ "unirig.predict_skin",
118
+ f"input.skeleton_path={temp_skeleton_path}", # Check exact Hydra param name in UniRig
119
+ f"input.source_mesh_path={input_glb_path}", # Check exact Hydra param name
120
+ f"output.path={temp_skin_path}",
121
+ ], "Skinning Prediction")
122
+ print(f"Skinning predicted at: {temp_skin_path}")
123
+ if not os.path.exists(temp_skin_path):
124
+ raise gr.Error("Skinning prediction failed to produce an output file.")
125
+
126
+ # Step 3: Merge Skeleton/Skin with Original Mesh
127
+ # Input: original GLB and the skin FBX (which contains skeleton + weights). Output: final rigged GLB
128
+ final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
129
+ print("Step 3: Merging Results...")
130
+ # Command: python -m unirig.merge_skeleton_skin +source_path=<temp_skin_path> +target_path=<input_glb_path> +output_path=<final_rigged_glb_path>
131
+ run_unirig_command([
132
+ "unirig.merge_skeleton_skin",
133
+ f"input.source_rig_path={temp_skin_path}", # Path to the file with skeleton and skin weights
134
+ f"input.target_mesh_path={input_glb_path}", # Path to the original mesh
135
+ f"output.path={final_rigged_glb_path}",
136
+ ], "Merging")
137
+ print(f"Final rigged mesh at: {final_rigged_glb_path}")
138
+ if not os.path.exists(final_rigged_glb_path):
139
+ raise gr.Error("Merging process failed to produce the final rigged GLB file.")
140
+
141
+ # The final_rigged_glb_path needs to be accessible by Gradio to serve it.
142
+ # Gradio usually copies temp files it creates, but here we created it.
143
+ # We return the path, and Gradio should handle it.
144
+ # The processing_temp_dir will be cleaned up by Gradio if input_glb_file_obj is from gr.File
145
+ # or we can clean it up if we copy the final file to a Gradio managed temp location.
146
+ # For gr.Model3D, returning a path is fine.
147
+ return final_rigged_glb_path
148
+
149
+ except gr.Error: # Re-raise Gradio errors directly
150
+ raise
151
  except Exception as e:
152
+ print(f"An unexpected error occurred: {e}")
153
+ # Clean up the processing directory in case of an unhandled error
154
+ if os.path.exists(processing_temp_dir):
155
+ shutil.rmtree(processing_temp_dir)
156
+ raise gr.Error(f"An unexpected error occurred during processing: {str(e)}")
157
+ # Note: Do not clean up processing_temp_dir in a `finally` block here if returning path from it,
158
+ # as Gradio needs the file to exist to serve it. Gradio's gr.File output type handles temp file cleanup.
159
+ # If outputting gr.File, copy the final file to a new tempfile managed by Gradio.
160
+ # For gr.Model3D, path is fine.
161
 
162
  # --- Gradio Interface ---
 
 
163
  theme = gr.themes.Soft(
164
+ primary_hue=gr.themes.colors.sky,
165
+ secondary_hue=gr.themes.colors.blue,
166
+ neutral_hue=gr.themes.colors.slate,
167
  font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
 
 
 
 
168
  )
169
 
 
170
  iface = gr.Interface(
171
+ fn=rig_glb_mesh_multistep,
172
+ inputs=gr.File(label="Upload .glb Mesh File", type="file"),
173
+ outputs=gr.Model3D(
174
+ label="Rigged 3D Model (.glb)",
175
+ clear_color=[0.8, 0.8, 0.8, 1.0],
176
+ # Note: Model3D might have issues with complex GLBs or certain rigging structures.
177
+ # A gr.File output for download might be a safer fallback.
178
+ # outputs=[gr.Model3D(...), gr.File(label="Download Rigged GLB")]
179
+ ),
180
+ title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
181
  description=(
182
+ "Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
183
+ "The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
184
+ "This may take several minutes. Ensure your GLB has clean geometry.\n"
185
+ f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{UNIRIG_REPO_DIR}'.\n"
186
  f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
187
  ),
188
+ cache_examples=False,
 
 
 
 
189
  theme=theme,
190
  allow_flagging="never"
191
  )
192
 
193
  if __name__ == "__main__":
194
+ # Perform a quick check for UniRig directory on launch
195
+ if not os.path.isdir(UNIRIG_REPO_DIR):
196
+ print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will likely fail.")
197
+ # You could display this error in the Gradio interface itself using a dummy function or Markdown.
198
+
199
+ # For local testing, you might need to set PYTHONPATH or ensure UniRig is installed.
200
+ # Example: os.environ["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{os.environ.get('PYTHONPATH', '')}"
201
+
202
  iface.launch()