jkorstad commited on
Commit
b864de5
·
verified ·
1 Parent(s): 882d74f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +185 -205
app.py CHANGED
@@ -1,205 +1,185 @@
1
- import gradio as gr
2
- import torch
3
- import os
4
- import sys
5
- import tempfile
6
- import shutil
7
- import subprocess
8
- import spaces
9
-
10
- # --- Configuration ---
11
- # Path to the cloned UniRig repository directory within the Space
12
- # IMPORTANT: You must clone the UniRig repository into this directory in your Hugging Face Space.
13
- UNIRIG_REPO_DIR = os.path.join(os.path.dirname(__file__), "UniRig")
14
-
15
- # Check if UniRig directory exists
16
- if not os.path.isdir(UNIRIG_REPO_DIR):
17
- # This message will appear in logs, Gradio app might fail to start fully.
18
- print(f"ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
19
- # Optionally, raise an error to make it more visible if the app starts
20
- # raise RuntimeError(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
21
-
22
-
23
- # Determine processing device (CUDA if available, otherwise CPU)
24
- DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
25
- print(f"Using device: {DEVICE}")
26
- if DEVICE.type == 'cuda':
27
- print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
28
- print(f"CUDA Version: {torch.version.cuda}")
29
- # Note: UniRig scripts might manage device internally or via Hydra configs.
30
- else:
31
- print("Warning: CUDA not available or not detected by PyTorch. UniRig performance will be severely impacted.")
32
-
33
- @spaces.GPU
34
- def run_unirig_command(command_args, step_name):
35
- """Helper function to run UniRig commands using subprocess."""
36
- python_exe = sys.executable # Use the current python interpreter
37
- cmd = [python_exe, "-m"] + command_args
38
-
39
- print(f"Running {step_name}: {' '.join(cmd)}")
40
-
41
- # UniRig scripts often expect to be run from the root of the UniRig repository
42
- # because they use Hydra and relative paths for configs (e.g., conf/config.yaml)
43
- process_env = os.environ.copy()
44
-
45
- # Add UniRig's parent directory to PYTHONPATH so `import unirig` works if needed,
46
- # and the UniRig directory itself so its internal imports work.
47
- # However, `python -m` typically handles package discovery well if CWD is correct.
48
- # process_env["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{process_env.get('PYTHONPATH', '')}"
49
-
50
- try:
51
- # Execute the command
52
- # It's crucial to set `cwd=UNIRIG_REPO_DIR` for Hydra to find its configs.
53
- result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
54
- print(f"{step_name} output:\n{result.stdout}")
55
- if result.stderr:
56
- print(f"{step_name} errors (non-fatal if check=True passed):\n{result.stderr}")
57
- except subprocess.CalledProcessError as e:
58
- print(f"ERROR during {step_name}:")
59
- print(f"Command: {' '.join(e.cmd)}")
60
- print(f"Return code: {e.returncode}")
61
- print(f"Stdout: {e.stdout}")
62
- print(f"Stderr: {e.stderr}")
63
- raise gr.Error(f"Error in UniRig {step_name}: {e.stderr[:500]}") # Show first 500 chars of error
64
- except FileNotFoundError:
65
- # This can happen if UNIRIG_REPO_DIR is not populated correctly or python_exe is wrong
66
- print(f"ERROR: Could not find executable or script for {step_name}. Is UniRig cloned correctly in {UNIRIG_REPO_DIR}?")
67
- raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs.")
68
-
69
-
70
- # --- Core Rigging Function ---
71
- @spaces.GPU
72
- def rig_glb_mesh_multistep(input_glb_file_obj):
73
- """
74
- Takes an input GLB file object, 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.")
79
-
80
- if input_glb_file_obj is None:
81
- raise gr.Error("No input file provided. Please upload a .glb mesh.")
82
-
83
- input_glb_path = input_glb_file_obj.name # Path to the temporary uploaded file
84
-
85
- # Create a dedicated temporary directory for all intermediate and final files for this run
86
- # This helps in organization and cleanup.
87
- processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
88
- print(f"Using temporary processing directory: {processing_temp_dir}")
89
-
90
- try:
91
- # Define paths for intermediate and final files within the processing_temp_dir
92
- # UniRig scripts expect output paths.
93
- base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
94
-
95
- # Step 1: Skeleton Prediction
96
- # Output is typically an FBX file for the skeleton
97
- temp_skeleton_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
98
- print("Step 1: Predicting Skeleton...")
99
- # Command: python -m unirig.predict_skeleton +input_path=<input_glb_path> +output_path=<temp_skeleton_path>
100
- # Note: UniRig's scripts might have default output locations or require specific Hydra overrides.
101
- # The `+` syntax is for Hydra overrides.
102
- # Check UniRig's `conf/predict_skeleton.yaml` for default config values.
103
- run_unirig_command([
104
- "unirig.predict_skeleton",
105
- f"input.path={input_glb_path}", # Use dot notation for Hydra parameters
106
- f"output.path={temp_skeleton_path}",
107
- # Add other necessary overrides, e.g., for device if not auto-detected well
108
- # f"device={str(DEVICE)}" # If UniRig's script accepts this override
109
- ], "Skeleton Prediction")
110
- print(f"Skeleton predicted at: {temp_skeleton_path}")
111
- if not os.path.exists(temp_skeleton_path):
112
- raise gr.Error("Skeleton prediction failed to produce an output file.")
113
-
114
- # Step 2: Skinning Weight Prediction
115
- # Input: skeleton FBX and original GLB. Output: skinned FBX (or other format)
116
- temp_skin_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
117
- print("Step 2: Predicting Skinning Weights...")
118
- # Command: python -m unirig.predict_skin +input_path=<temp_skeleton_path> +output_path=<temp_skin_path> +source_mesh_path=<input_glb_path>
119
- run_unirig_command([
120
- "unirig.predict_skin",
121
- f"input.skeleton_path={temp_skeleton_path}", # Check exact Hydra param name in UniRig
122
- f"input.source_mesh_path={input_glb_path}", # Check exact Hydra param name
123
- f"output.path={temp_skin_path}",
124
- ], "Skinning Prediction")
125
- print(f"Skinning predicted at: {temp_skin_path}")
126
- if not os.path.exists(temp_skin_path):
127
- raise gr.Error("Skinning prediction failed to produce an output file.")
128
-
129
- # Step 3: Merge Skeleton/Skin with Original Mesh
130
- # Input: original GLB and the skin FBX (which contains skeleton + weights). Output: final rigged GLB
131
- final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
132
- print("Step 3: Merging Results...")
133
- # Command: python -m unirig.merge_skeleton_skin +source_path=<temp_skin_path> +target_path=<input_glb_path> +output_path=<final_rigged_glb_path>
134
- run_unirig_command([
135
- "unirig.merge_skeleton_skin",
136
- f"input.source_rig_path={temp_skin_path}", # Path to the file with skeleton and skin weights
137
- f"input.target_mesh_path={input_glb_path}", # Path to the original mesh
138
- f"output.path={final_rigged_glb_path}",
139
- ], "Merging")
140
- print(f"Final rigged mesh at: {final_rigged_glb_path}")
141
- if not os.path.exists(final_rigged_glb_path):
142
- raise gr.Error("Merging process failed to produce the final rigged GLB file.")
143
-
144
- # The final_rigged_glb_path needs to be accessible by Gradio to serve it.
145
- # Gradio usually copies temp files it creates, but here we created it.
146
- # We return the path, and Gradio should handle it.
147
- # The processing_temp_dir will be cleaned up by Gradio if input_glb_file_obj is from gr.File
148
- # or we can clean it up if we copy the final file to a Gradio managed temp location.
149
- # For gr.Model3D, returning a path is fine.
150
- return final_rigged_glb_path
151
-
152
- except gr.Error: # Re-raise Gradio errors directly
153
- raise
154
- except Exception as e:
155
- print(f"An unexpected error occurred: {e}")
156
- # Clean up the processing directory in case of an unhandled error
157
- if os.path.exists(processing_temp_dir):
158
- shutil.rmtree(processing_temp_dir)
159
- raise gr.Error(f"An unexpected error occurred during processing: {str(e)}")
160
- # Note: Do not clean up processing_temp_dir in a `finally` block here if returning path from it,
161
- # as Gradio needs the file to exist to serve it. Gradio's gr.File output type handles temp file cleanup.
162
- # If outputting gr.File, copy the final file to a new tempfile managed by Gradio.
163
- # For gr.Model3D, path is fine.
164
-
165
- # --- Gradio Interface ---
166
- theme = gr.themes.Soft(
167
- primary_hue=gr.themes.colors.sky,
168
- secondary_hue=gr.themes.colors.blue,
169
- neutral_hue=gr.themes.colors.slate,
170
- font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
171
- )
172
-
173
- iface = gr.Interface(
174
- fn=rig_glb_mesh_multistep,
175
- inputs=gr.File(label="Upload .glb Mesh File", type="file"),
176
- outputs=gr.Model3D(
177
- label="Rigged 3D Model (.glb)",
178
- clear_color=[0.8, 0.8, 0.8, 1.0],
179
- # Note: Model3D might have issues with complex GLBs or certain rigging structures.
180
- # A gr.File output for download might be a safer fallback.
181
- # outputs=[gr.Model3D(...), gr.File(label="Download Rigged GLB")]
182
- ),
183
- title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
184
- description=(
185
- "Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
186
- "The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
187
- "This may take several minutes. Ensure your GLB has clean geometry.\n"
188
- f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{UNIRIG_REPO_DIR}'.\n"
189
- f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
190
- ),
191
- cache_examples=False,
192
- theme=theme,
193
- allow_flagging="never"
194
- )
195
-
196
- if __name__ == "__main__":
197
- # Perform a quick check for UniRig directory on launch
198
- if not os.path.isdir(UNIRIG_REPO_DIR):
199
- print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will likely fail.")
200
- # You could display this error in the Gradio interface itself using a dummy function or Markdown.
201
-
202
- # For local testing, you might need to set PYTHONPATH or ensure UniRig is installed.
203
- # Example: os.environ["PYTHONPATH"] = f"{UNIRIG_REPO_DIR}{os.pathsep}{os.environ.get('PYTHONPATH', '')}"
204
-
205
- 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
+ # 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
+
11
+ # --- Configuration ---
12
+ # Path to the cloned UniRig repository directory within the Space
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
+ # It's generally better to ensure UniRig's internal scripts handle PYTHONPATH if needed,
37
+ # or that it's installed in a way that `python -m` works correctly from its root.
38
+ # Setting cwd=UNIRIG_REPO_DIR is often the key for Hydra.
39
+
40
+ try:
41
+ # Execute the command from the UniRig directory for Hydra to find configs
42
+ result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
43
+ print(f"{step_name} STDOUT:\n{result.stdout}")
44
+ if result.stderr:
45
+ print(f"{step_name} STDERR (non-fatal or warnings):\n{result.stderr}")
46
+ except subprocess.CalledProcessError as e:
47
+ print(f"ERROR during {step_name}:")
48
+ print(f"Command: {' '.join(e.cmd)}")
49
+ print(f"Return code: {e.returncode}")
50
+ print(f"Stdout: {e.stdout}")
51
+ print(f"Stderr: {e.stderr}")
52
+ # Provide a more user-friendly error, potentially masking long tracebacks
53
+ error_summary = e.stderr.splitlines()[-5:] # Last 5 lines of stderr
54
+ raise gr.Error(f"Error in UniRig {step_name}. Details: {' '.join(error_summary)}")
55
+ except FileNotFoundError:
56
+ print(f"ERROR: Could not find executable or script for {step_name}. Is UniRig cloned correctly in {UNIRIG_REPO_DIR} and Python environment setup?")
57
+ raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs and UniRig directory structure.")
58
+ except Exception as e_general:
59
+ print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
60
+ raise gr.Error(f"Unexpected Python error during {step_name}: {str(e_general)[:500]}")
61
+
62
+
63
+ # If you are using @spaces.GPU, you would import it:
64
+ # import spaces
65
+ # @spaces.GPU # You can specify type like @spaces.GPU(type="t4") or count
66
+ def rig_glb_mesh_multistep(input_glb_file_obj):
67
+ """
68
+ Takes an input GLB file object (from gr.File with type="filepath"),
69
+ rigs it using the new UniRig multi-step process,
70
+ and returns the path to the final rigged GLB file.
71
+ """
72
+ if not os.path.isdir(UNIRIG_REPO_DIR):
73
+ raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}. Cannot proceed. Please check Space setup.")
74
+
75
+ if input_glb_file_obj is None:
76
+ # This case should ideally be handled by Gradio's input validation if `allow_none=False` (default)
77
+ raise gr.Error("No input file provided. Please upload a .glb mesh.")
78
+
79
+ # When type="filepath", input_glb_file_obj is the path string directly
80
+ input_glb_path = input_glb_file_obj
81
+ print(f"Input GLB path received: {input_glb_path}")
82
+
83
+ # Create a dedicated temporary directory for all intermediate and final files
84
+ processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
85
+ print(f"Using temporary processing directory: {processing_temp_dir}")
86
+
87
+ try:
88
+ base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
89
+
90
+ # Step 1: Skeleton Prediction
91
+ temp_skeleton_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
92
+ print("Step 1: Predicting Skeleton...")
93
+ run_unirig_command([
94
+ "unirig.predict_skeleton",
95
+ f"input.path={os.path.abspath(input_glb_path)}", # Use absolute path for robustness
96
+ f"output.path={os.path.abspath(temp_skeleton_path)}",
97
+ # f"device={str(DEVICE)}" # If UniRig's script accepts this override and handles it
98
+ ], "Skeleton Prediction")
99
+ if not os.path.exists(temp_skeleton_path):
100
+ raise gr.Error("Skeleton prediction failed to produce an output file. Check logs for UniRig errors.")
101
+
102
+ # Step 2: Skinning Weight Prediction
103
+ temp_skin_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
104
+ print("Step 2: Predicting Skinning Weights...")
105
+ run_unirig_command([
106
+ "unirig.predict_skin",
107
+ f"input.skeleton_path={os.path.abspath(temp_skeleton_path)}",
108
+ f"input.source_mesh_path={os.path.abspath(input_glb_path)}",
109
+ f"output.path={os.path.abspath(temp_skin_path)}",
110
+ ], "Skinning Prediction")
111
+ if not os.path.exists(temp_skin_path):
112
+ raise gr.Error("Skinning prediction failed to produce an output file. Check logs for UniRig errors.")
113
+
114
+ # Step 3: Merge Skeleton/Skin with Original Mesh
115
+ final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")
116
+ print("Step 3: Merging Results...")
117
+ run_unirig_command([
118
+ "unirig.merge_skeleton_skin",
119
+ f"input.source_rig_path={os.path.abspath(temp_skin_path)}",
120
+ f"input.target_mesh_path={os.path.abspath(input_glb_path)}",
121
+ f"output.path={os.path.abspath(final_rigged_glb_path)}",
122
+ ], "Merging")
123
+ if not os.path.exists(final_rigged_glb_path):
124
+ raise gr.Error("Merging process failed to produce the final rigged GLB file. Check logs for UniRig errors.")
125
+
126
+ # final_rigged_glb_path is in processing_temp_dir.
127
+ # Gradio's gr.Model3D output component will handle serving this file.
128
+ return final_rigged_glb_path
129
+
130
+ except gr.Error: # Re-raise Gradio errors directly
131
+ if os.path.exists(processing_temp_dir): # Clean up on known Gradio error
132
+ shutil.rmtree(processing_temp_dir)
133
+ print(f"Cleaned up temporary directory: {processing_temp_dir}")
134
+ raise
135
+ except Exception as e:
136
+ print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
137
+ if os.path.exists(processing_temp_dir): # Clean up on unexpected error
138
+ shutil.rmtree(processing_temp_dir)
139
+ print(f"Cleaned up temporary directory: {processing_temp_dir}")
140
+ raise gr.Error(f"An unexpected error occurred during processing: {str(e)[:500]}")
141
+
142
+
143
+ # --- Gradio Interface ---
144
+ theme = gr.themes.Soft(
145
+ primary_hue=gr.themes.colors.sky,
146
+ secondary_hue=gr.themes.colors.blue,
147
+ neutral_hue=gr.themes.colors.slate,
148
+ font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
149
+ )
150
+
151
+ # Ensure UNIRIG_REPO_DIR check happens before interface is built if it's critical
152
+ if not os.path.isdir(UNIRIG_REPO_DIR) and __name__ == "__main__": # Check only if running as main script
153
+ print(f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. The application will not work.")
154
+
155
+ # Define the interface
156
+ # Note: The @spaces.GPU decorator would go above the function `rig_glb_mesh_multistep`
157
+ iface = gr.Interface(
158
+ fn=rig_glb_mesh_multistep,
159
+ inputs=gr.File(
160
+ label="Upload .glb Mesh File",
161
+ type="filepath" # Corrected type for Gradio 5.x.x
162
+ ),
163
+ outputs=gr.Model3D(
164
+ label="Rigged 3D Model (.glb)",
165
+ clear_color=[0.8, 0.8, 0.8, 1.0],
166
+ ),
167
+ title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
168
+ description=(
169
+ "Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
170
+ "The process involves: 1. Skeleton Prediction, 2. Skinning Weight Prediction, 3. Merging.\n"
171
+ "This may take several minutes. Ensure your GLB has clean geometry.\n"
172
+ f"Running on: {str(DEVICE).upper()}. UniRig repo expected at: '{os.path.basename(UNIRIG_REPO_DIR)}'.\n"
173
+ f"UniRig Source: https://github.com/VAST-AI-Research/UniRig"
174
+ ),
175
+ cache_examples=False,
176
+ theme=theme,
177
+ allow_flagging="never"
178
+ )
179
+
180
+ if __name__ == "__main__":
181
+ if not os.path.isdir(UNIRIG_REPO_DIR):
182
+ print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}. Ensure it's cloned in the Space's root.")
183
+
184
+ iface.launch()
185
+