File size: 9,237 Bytes
498b8bb
 
 
 
 
 
 
e444307
e843bc1
498b8bb
 
 
 
 
e843bc1
 
 
fa43ea8
 
080c217
 
 
 
 
 
 
 
 
 
25e014c
 
 
 
 
 
 
 
 
 
498b8bb
fd9d8fc
 
498b8bb
 
 
 
fd9d8fc
 
498b8bb
fd9d8fc
498b8bb
25e014c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e7da273
 
fa43ea8
 
fd9d8fc
 
 
 
 
e7da273
 
 
25e014c
 
fa43ea8
 
 
25e014c
fd9d8fc
 
 
 
 
 
 
 
 
 
 
 
 
25e014c
fd9d8fc
 
fa43ea8
 
fd9d8fc
 
 
 
fa43ea8
498b8bb
fa43ea8
 
25e014c
fd9d8fc
fa43ea8
fd9d8fc
 
 
 
25e014c
fd9d8fc
 
 
 
 
 
 
e7da273
 
 
 
25e014c
fd9d8fc
fa43ea8
e7da273
 
fd9d8fc
 
25e014c
fd9d8fc
fa43ea8
e7da273
 
fa43ea8
fd9d8fc
25e014c
fd9d8fc
fa43ea8
e7da273
 
fa43ea8
fd9d8fc
e7da273
fd9d8fc
25e014c
 
fd9d8fc
 
 
 
 
25e014c
fd9d8fc
 
 
498b8bb
 
 
fd9d8fc
 
 
 
498b8bb
 
25e014c
fa43ea8
498b8bb
 
25e014c
fa43ea8
 
fd9d8fc
 
fa43ea8
 
 
fd9d8fc
 
 
498b8bb
 
 
fd9d8fc
fa43ea8
e7da273
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import gradio as gr
import torch
import os
import sys
import tempfile
import shutil
import subprocess
import spaces
from typing import Any, Dict, Union

# --- Configuration ---
# Path to the cloned UniRig repository directory within the Space
UNIRIG_REPO_DIR = os.path.join(os.path.dirname(__file__), "UniRig")

# Path to the Blender Python site-packages
BLENDER_PYTHON_PATH = "/opt/blender-4.2.0-linux-x64/4.2/python/lib/python3.11/site-packages"

BLENDER_PYTHON_EXEC = "/opt/blender-4.2.0-linux-x64/4.2/python/bin/python3.11"

# Path to the setup script
SETUP_SCRIPT = os.path.join(os.path.dirname(__file__), "setup_blender.sh")

# Check if Blender is installed
if not os.path.exists("/usr/local/bin/blender"):
    print("Blender not found. Installing...")
    subprocess.run(["bash", SETUP_SCRIPT], check=True)
else:
    print("Blender is already installed.")

# Verify Blender Python path
if os.path.exists(BLENDER_PYTHON_PATH):
    print(f"Blender Python modules found at {BLENDER_PYTHON_PATH}")
    if os.path.exists(os.path.join(BLENDER_PYTHON_PATH, "bpy.so")):
        print("bpy.so found - Blender Python integration should work")
    else:
        print("bpy.so not found - Check Blender installation or path")
else:
    print(f"Blender Python modules not found at {BLENDER_PYTHON_PATH} - Update BLENDER_PYTHON_PATH")

if not os.path.isdir(UNIRIG_REPO_DIR):
    print(f"ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}. Please clone it there.")
    # Consider raising an error or displaying it in the UI if UniRig is critical for startup

DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {DEVICE}")
if DEVICE.type == 'cuda':
    print(f"CUDA Device Name: {torch.cuda.get_device_name(0)}")
    print(f"CUDA Version: {torch.version.cuda}")
else:
    print("Warning: CUDA not available or not detected by PyTorch. UniRig performance will be severely impacted.")

def patch_asset_py():
    """Temporary patch to fix type hinting error in asset.py"""
    asset_py_path = os.path.join(UNIRIG_REPO_DIR, "src", "data", "asset.py")
    try:
        with open(asset_py_path, "r") as f:
            content = f.read()
        # Check if patch is already applied
        if "meta: Union[Dict[str, Any], None]" in content:
            print("Patch already applied to asset.py")
            return
        # Replace the problematic line
        content = content.replace("meta: Union[Dict[str, ...], None]=None", "meta: Union[Dict[str, Any], None]=None")
        # Ensure 'Any' is imported
        if "from typing import Any" not in content:
            content = "from typing import Any\n" + content
        with open(asset_py_path, "w") as f:
            f.write(content)
        print("Successfully patched asset.py")
    except Exception as e:
        print(f"Failed to patch asset.py: {e}")
        raise

@spaces.GPU # Decorator for ZeroGPU
def run_unirig_command(command_list, step_name):
    """Run UniRig commands using Blender's Python interpreter."""
    cmd = [BLENDER_PYTHON_EXEC] + command_list[1:]  # Use Blender's Python instead of bash!!
    
    print(f"Running {step_name}: {' '.join(cmd)}")
    
    process_env = os.environ.copy()
    
    # Determine the path to the 'src' directory within UniRig, where the 'unirig' package resides.
    unirig_src_dir = os.path.join(UNIRIG_REPO_DIR, "src")

    # Explicitly add BLENDER_PYTHON_PATH, UNIRIG_REPO_DIR/src, and UNIRIG_REPO_DIR to PYTHONPATH
    new_pythonpath_parts = [BLENDER_PYTHON_PATH, unirig_src_dir, UNIRIG_REPO_DIR]
    #existing_pythonpath = process_env.get('PYTHONPATH', '')
    #if existing_pythonpath:
    #    new_pythonpath_parts.extend(existing_pythonpath.split(os.pathsep))
    process_env["PYTHONPATH"] = os.pathsep.join(filter(None, new_pythonpath_parts))
    print(f"Set PYTHONPATH for subprocess: {process_env['PYTHONPATH']}")

    try:
        result = subprocess.run(cmd, cwd=UNIRIG_REPO_DIR, capture_output=True, text=True, check=True, env=process_env)
        print(f"{step_name} STDOUT:\n{result.stdout}")
        if result.stderr:
            print(f"{step_name} STDERR (non-fatal or warnings):\n{result.stderr}")
    except subprocess.CalledProcessError as e:
        print(f"ERROR during {step_name}:")
        print(f"Command: {' '.join(e.cmd)}")
        print(f"Return code: {e.returncode}")
        print(f"Stdout: {e.stdout}")
        print(f"Stderr: {e.stderr}")
        error_summary = e.stderr.splitlines()[-5:]
        raise gr.Error(f"Error in UniRig {step_name}. Details: {' '.join(error_summary)}")
    except FileNotFoundError:
        print(f"ERROR: Could not find executable or script for {step_name}. Command: {' '.join(cmd)}")
        raise gr.Error(f"Setup error for UniRig {step_name}. Check server logs and script paths.")
    except Exception as e_general:
        print(f"An unexpected Python exception occurred in run_unirig_command for {step_name}: {e_general}")
        raise gr.Error(f"Unexpected Python error during {step_name}: {str(e_general)[:500]}")

@spaces.GPU
def rig_glb_mesh_multistep(input_glb_file_obj):
    """Rig a GLB mesh using UniRig's multi-step process."""
    patch_asset_py()

    if not os.path.isdir(UNIRIG_REPO_DIR):
        raise gr.Error(f"UniRig repository not found at {UNIRIG_REPO_DIR}.")

    if input_glb_file_obj is None:
        raise gr.Error("No input file provided. Please upload a .glb mesh.")

    input_glb_path = input_glb_file_obj
    print(f"Input GLB path received: {input_glb_path}")

    processing_temp_dir = tempfile.mkdtemp(prefix="unirig_processing_")
    print(f"Using temporary processing directory: {processing_temp_dir}")

    try:
        base_name = os.path.splitext(os.path.basename(input_glb_path))[0]
        abs_skeleton_output_path = os.path.join(processing_temp_dir, f"{base_name}_skeleton.fbx")
        abs_skin_output_path = os.path.join(processing_temp_dir, f"{base_name}_skin.fbx")
        abs_final_rigged_glb_path = os.path.join(processing_temp_dir, f"{base_name}_rigged_final.glb")

        # Step 1: Skeleton Prediction
        print("Step 1: Predicting Skeleton...")
        skeleton_cmd = ["python", "launch/inference/generate_skeleton.py", "--input", input_glb_path, "--output", abs_skeleton_output_path]
        run_unirig_command(skeleton_cmd, "Skeleton Prediction")
        if not os.path.exists(abs_skeleton_output_path):
            raise gr.Error("Skeleton prediction failed to produce an output file. Check logs for UniRig errors.")

        # Step 2: Skinning Weight Prediction
        print("Step 2: Predicting Skinning Weights...")
        skin_cmd = ["python", "launch/inference/generate_skin.py", "--input", abs_skeleton_output_path, "--source", input_glb_path, "--output", abs_skin_output_path]
        run_unirig_command(skin_cmd, "Skinning Prediction")
        if not os.path.exists(abs_skin_output_path):
            raise gr.Error("Skinning prediction failed to produce an output file.")

        # Step 3: Merge Skeleton/Skin with Original Mesh
        print("Step 3: Merging Results...")
        merge_cmd = ["python", "launch/inference/merge.py", "--source", abs_skin_output_path, "--target", input_glb_path, "--output", abs_final_rigged_glb_path]
        run_unirig_command(merge_cmd, "Merging")
        if not os.path.exists(abs_final_rigged_glb_path):
            raise gr.Error("Merging process failed to produce the final rigged GLB file.")

        return abs_final_rigged_glb_path

    except gr.Error:
        if os.path.exists(processing_temp_dir):
            shutil.rmtree(processing_temp_dir)
            print(f"Cleaned up temporary directory: {processing_temp_dir}")
        raise
    except Exception as e:
        print(f"An unexpected error occurred in rig_glb_mesh_multistep: {e}")
        if os.path.exists(processing_temp_dir):
            shutil.rmtree(processing_temp_dir)
            print(f"Cleaned up temporary directory: {processing_temp_dir}")
        raise gr.Error(f"An unexpected error occurred during processing: {str(e)[:500]}")

# --- Gradio Interface ---
theme = gr.themes.Soft(
    primary_hue=gr.themes.colors.sky,
    secondary_hue=gr.themes.colors.blue,
    neutral_hue=gr.themes.colors.slate,
    font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
)

if not os.path.isdir(UNIRIG_REPO_DIR) and __name__ == "__main__":
    print(f"CRITICAL STARTUP ERROR: UniRig repository not found at {UNIRIG_REPO_DIR}.")

iface = gr.Interface(
    fn=rig_glb_mesh_multistep,
    inputs=gr.File(label="Upload .glb Mesh File", type="filepath"),
    outputs=gr.Model3D(label="Rigged 3D Model (.glb)", clear_color=[0.8, 0.8, 0.8, 1.0]),
    title="UniRig Auto-Rigger (Python 3.11 / PyTorch 2.3+)",
    description=(
        "Upload a 3D mesh in `.glb` format. This application uses the latest UniRig to automatically rig the mesh.\n"
        "Running on: {DEVICE.upper()}. UniRig repo expected at: '{os.path.basename(UNIRIG_REPO_DIR)}'.\n"
        "UniRig Source: https://github.com/VAST-AI-Research/UniRig"
    ),
    cache_examples=False,
    theme=theme
)

if __name__ == "__main__":
    if not os.path.isdir(UNIRIG_REPO_DIR):
        print(f"CRITICAL: UniRig repository not found at {UNIRIG_REPO_DIR}.")
    iface.launch()