Spaces:
Running
Running
Download patch_runtime.py from STATICAI/Motion-Rig: direct link, hf CLI and curl.
- Browser
- Download file 15 kB
-
https://huggingface.co/spaces/STATICAI/Motion-Rig/resolve/main/patch_runtime.py
- Command line
-
hf download hf://spaces/STATICAI/Motion-Rig/patch_runtime.py
-
curl -L -o patch_runtime.py https://huggingface.co/spaces/STATICAI/Motion-Rig/resolve/main/patch_runtime.py
15 kB
| from pathlib import Path | |
| import os | |
| import subprocess | |
| # The upstream checkout lives on persistent storage and therefore retains edits | |
| # across container restarts. Restore only the files managed by this patcher from | |
| # its pinned Git commit before applying changes, making this script idempotent. | |
| for relative_path in ( | |
| "util/dataset_mixamo_additional.py", | |
| "model.py", | |
| "models_ae.py", | |
| "app.py", | |
| "app_blender.py", | |
| "util/blender_utils.py", | |
| ): | |
| pristine = subprocess.check_output( | |
| ["git", "-c", "safe.directory=/data/app", "-C", "app", "show", f"HEAD:{relative_path}"] | |
| ) | |
| Path("app", relative_path).write_bytes(pristine) | |
| # ββ 1. dataset_mixamo_additional.py β optional extended skeleton guard βββββββββ | |
| path = Path("app/util/dataset_mixamo_additional.py") | |
| if path.exists(): | |
| source = path.read_text() | |
| old = """TEMPLATE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../data/Mixamo/bones_vroid.fbx")) | |
| KINEMATIC_TREE = get_kinematic_tree(TEMPLATE_PATH, BONES_IDX_DICT) | |
| assert len(KINEMATIC_TREE) == len(MIXAMO_JOINTS) | |
| """ | |
| new = """TEMPLATE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../data/Mixamo/bones_vroid.fbx")) | |
| if os.path.isfile(TEMPLATE_PATH): | |
| KINEMATIC_TREE = get_kinematic_tree(TEMPLATE_PATH, BONES_IDX_DICT) | |
| assert len(KINEMATIC_TREE) == len(MIXAMO_JOINTS) | |
| else: | |
| # The optional extended skeleton lives in the restricted Mixamo dataset. | |
| # Standard humanoid inference does not use it (joints_additional=False). | |
| KINEMATIC_TREE = dataset_mixamo.KINEMATIC_TREE | |
| """ | |
| if old in source: | |
| path.write_text(source.replace(old, new)) | |
| print("[dataset_mixamo_additional.py] extended-skeleton guard patched") | |
| else: | |
| print("[dataset_mixamo_additional.py] already patched or block not found") | |
| # ββ fps fallback: replaces torch_cluster.fps with pytorch3d equivalent βββββββββ | |
| FPS_FALLBACK = """try: | |
| from torch_cluster import fps | |
| except ImportError: | |
| import numpy as np | |
| from pytorch3d.ops import sample_farthest_points | |
| def fps(x, batch, ratio): | |
| import torch | |
| sampled = [] | |
| for batch_id in torch.unique(batch, sorted=True): | |
| global_idx = torch.nonzero(batch == batch_id, as_tuple=False).flatten() | |
| points = x[global_idx].unsqueeze(0) | |
| count = max(1, int(np.ceil(points.shape[1] * ratio))) | |
| _, local_idx = sample_farthest_points(points, K=count, random_start_point=True) | |
| sampled.append(global_idx[local_idx[0]]) | |
| return torch.cat(sampled) | |
| """ | |
| CLUSTER_IMPORT = "from torch_cluster import fps" | |
| # ββ 2. model.py βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| model_path = Path("app/model.py") | |
| if model_path.exists(): | |
| src = model_path.read_text() | |
| if CLUSTER_IMPORT in src: | |
| model_path.write_text(src.replace(CLUSTER_IMPORT, FPS_FALLBACK, 1)) | |
| print("[model.py] torch_cluster fps patched") | |
| else: | |
| print("[model.py] already patched") | |
| else: | |
| print("[model.py] not found") | |
| # ββ 3. models_ae.py β THE CRITICAL MISSING PATCH βββββββββββββββββββββββββββββ | |
| # The original patch_runtime.py only patched model.py but NOT models_ae.py, | |
| # which also imports torch_cluster. This caused the ModuleNotFoundError. | |
| models_ae_path = Path("app/models_ae.py") | |
| if models_ae_path.exists(): | |
| src = models_ae_path.read_text() | |
| if CLUSTER_IMPORT in src: | |
| models_ae_path.write_text(src.replace(CLUSTER_IMPORT, FPS_FALLBACK, 1)) | |
| print("[models_ae.py] torch_cluster fps patched") | |
| else: | |
| print("[models_ae.py] already patched") | |
| else: | |
| print("[models_ae.py] not found") | |
| # ββ 4. app/app.py β @spaces.GPU duration βββββββββββββββββββββββββββββββββββββ | |
| app_path = Path("app/app.py") | |
| if app_path.exists(): | |
| src = app_path.read_text() | |
| bare_gpu_decorators = src.count("@spaces.GPU\n") | |
| if bare_gpu_decorators != 2: | |
| raise RuntimeError(f"Expected 2 bare GPU decorators, found {bare_gpu_decorators}") | |
| app_path.write_text(src.replace("@spaces.GPU\n", "@spaces.GPU(duration=300)\n")) | |
| print(f"[app/app.py] {bare_gpu_decorators} @spaces.GPU -> duration=300 patched") | |
| else: | |
| print("[app/app.py] not found") | |
| print("patch_runtime.py done.") | |
| # STATICAI Standard animation library integration | |
| def _replace_once(path, old, new, label): | |
| source = path.read_text() | |
| count = source.count(old) | |
| if count != 1: | |
| raise RuntimeError(f"{label}: expected exactly one patch target, found {count}") | |
| path.write_text(source.replace(old, new, 1)) | |
| blender_utils_path = Path("app/util/blender_utils.py") | |
| _replace_once( | |
| blender_utils_path, | |
| "def load_mixamo_anim(char_file: str, anim_file: str, do_retarget=False, inplace=False, to_tris=False):", | |
| "def load_mixamo_anim(char_file: str, anim_file: str, do_retarget=False, inplace=False, to_tris=False, action_name=None):", | |
| "blender_utils signature", | |
| ) | |
| _replace_once( | |
| blender_utils_path, | |
| ' assert anim_armature.animation_data is not None and len(bpy.data.actions) > 0, f"Animation not found in {anim_file}"\n\n set_action(char_armature, anim_armature.animation_data.action)', | |
| ''' assert anim_armature.animation_data is not None and len(bpy.data.actions) > 0, f"Animation not found in {anim_file}" | |
| if action_name: | |
| candidates = [ | |
| action | |
| for action in bpy.data.actions | |
| if action.name == action_name | |
| or action.name.endswith("|" + action_name) | |
| or action.name.startswith(action_name + ".") | |
| ] | |
| if not candidates: | |
| available = ", ".join(sorted(action.name for action in bpy.data.actions)) | |
| raise ValueError(f"Animation action {action_name!r} not found. Available: {available}") | |
| anim_armature.animation_data.action = candidates[0] | |
| print(f"Selected animation action: {candidates[0].name}") | |
| set_action(char_armature, anim_armature.animation_data.action)''', | |
| "blender_utils action selection", | |
| ) | |
| app_blender_path = Path("app/app_blender.py") | |
| _replace_once( | |
| app_blender_path, | |
| ''' blender_utils.load_mixamo_anim( | |
| [armature_obj, mesh_obj], args.animation_path, do_retarget=args.retarget, inplace=args.inplace | |
| )''', | |
| ''' blender_utils.load_mixamo_anim( | |
| [armature_obj, mesh_obj], | |
| args.animation_path, | |
| do_retarget=args.retarget, | |
| inplace=args.inplace, | |
| action_name=args.animation_action, | |
| )''', | |
| "app_blender load_mixamo_anim", | |
| ) | |
| _replace_once( | |
| app_blender_path, | |
| ' parser.add_argument("--animation_path", type=str, default=None)\n', | |
| ' parser.add_argument("--animation_path", type=str, default=None)\n parser.add_argument("--animation_action", type=str, default=None)\n', | |
| "app_blender parser action", | |
| ) | |
| app_path = Path("app/app.py") | |
| _replace_once(app_path, "import gc\nimport os\n", "import gc\nimport json\nimport os\n", "app json import") | |
| _replace_once( | |
| app_path, | |
| ''' animation_file: str, | |
| retarget: bool,''', | |
| ''' animation_file: str, | |
| animation_action: str, | |
| retarget: bool,''', | |
| "vis_blender action argument", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' animation_path=animation_file, | |
| retarget=retarget,''', | |
| ''' animation_path=animation_file, | |
| animation_action=animation_action, | |
| retarget=retarget,''', | |
| "Namespace action argument", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' if animation_file is not None: | |
| cmd += f" --animation_path '{os.path.abspath(animation_file)}'" | |
| if retarget: | |
| cmd += " --retarget" | |
| if inplace: | |
| cmd += " --inplace"''', | |
| ''' if animation_file is not None: | |
| cmd += f" --animation_path '{os.path.abspath(animation_file)}'" | |
| if animation_action: | |
| safe_action = animation_action.replace("'", "") | |
| cmd += f" --animation_action '{safe_action}'" | |
| if retarget: | |
| cmd += " --retarget" | |
| if inplace: | |
| cmd += " --inplace"''', | |
| "subprocess action argument", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' animation_file: str = None, | |
| retarget=True,''', | |
| ''' animation_file: str = None, | |
| animation_action: str = None, | |
| retarget=True,''', | |
| "pipeline action argument", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' yield vis_blender( | |
| reset_to_rest, no_fingers, rest_pose_type, ignore_pose_parts, animation_file, retarget, inplace, db | |
| )''', | |
| ''' yield vis_blender( | |
| reset_to_rest, | |
| no_fingers, | |
| rest_pose_type, | |
| ignore_pose_parts, | |
| animation_file, | |
| animation_action, | |
| retarget, | |
| inplace, | |
| db, | |
| )''', | |
| "pipeline vis_blender action", | |
| ) | |
| _replace_once(app_path, ' title = "Make-It-Animatable"\n', ' title = "Motion Rig"\n', "Motion Rig title") | |
| _replace_once( | |
| app_path, | |
| ' animation_dir = "data/Mixamo/animation"\n', | |
| ''' animation_dir = "data/Mixamo/animation" | |
| standard_library_root = "/data/animations/Standard" | |
| standard_manifest_path = os.path.join(standard_library_root, "ANIMATION_MANIFEST.json") | |
| standard_animation_lookup = {} | |
| standard_animation_choices = [] | |
| if os.path.isfile(standard_manifest_path): | |
| with open(standard_manifest_path, "r", encoding="utf-8") as fh: | |
| manifest = json.load(fh) | |
| for entry_index, entry in enumerate(manifest.get("entries", [])): | |
| motion_file = os.path.join(standard_library_root, entry["file"]) | |
| variant = "Root Motion" if entry.get("root_motion") else "In Place" | |
| for action in entry.get("actions", []): | |
| if not action or action == "A_TPose": | |
| continue | |
| key = f"{entry_index}:{action}" | |
| label = f"Standard {entry.get('set')} | {action} | {variant}" | |
| standard_animation_lookup[key] = {"path": motion_file, "action": action} | |
| standard_animation_choices.append((label, key)) | |
| ''', | |
| "Standard library manifest load", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ' sorted(glob("*.fbx", root_dir=animation_dir)),\n', | |
| ' standard_animation_choices,\n', | |
| "Standard animation choices", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ' label="Select Animation File",\n', | |
| ' label="Standard Animation Library",\n', | |
| "Standard animation label", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' input_animation_file = gr.File( | |
| label="Animation File", | |
| file_types=[".fbx"], | |
| value=lambda: "./data/Standard Run.fbx", | |
| interactive=True, | |
| )''', | |
| ''' input_animation_file = gr.File( | |
| label="Animation File", | |
| file_types=[".fbx"], | |
| value=lambda: "./data/Standard Run.fbx", | |
| interactive=True, | |
| ) | |
| animation_action_state = gr.State(value=None)''', | |
| "animation action state", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' input_animation_file, | |
| input_retarget,''', | |
| ''' input_animation_file, | |
| animation_action_state, | |
| input_retarget,''', | |
| "inputs tuple action state", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' animation_file=inputs[input_animation_file], | |
| retarget=inputs[input_retarget],''', | |
| ''' animation_file=inputs[input_animation_file], | |
| animation_action=inputs[animation_action_state], | |
| retarget=inputs[input_retarget],''', | |
| "pipeline UI action", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' input_animation_file, | |
| input_retarget,''', | |
| ''' input_animation_file, | |
| animation_action_state, | |
| input_retarget,''', | |
| "animate inputs action state", | |
| ) | |
| _replace_once( | |
| app_path, | |
| ''' def select2file(selected: str): | |
| return None if selected is None else os.path.join(animation_dir, selected) | |
| select_animation_file.input( | |
| lambda x: (select2file(x), gr.Checkbox(value=True)), | |
| inputs=select_animation_file, | |
| outputs=[input_animation_file, input_reset_to_rest], | |
| preprocess=False, | |
| ) | |
| input_animation_file.upload( | |
| lambda: (gr.Dropdown(value=None), gr.Checkbox(value=True)), | |
| outputs=[select_animation_file, input_reset_to_rest], | |
| ) | |
| input_animation_file.clear( | |
| lambda: gr.Dropdown(value=None), outputs=select_animation_file, show_progress="hidden" | |
| )''', | |
| ''' def select2file(selected: str): | |
| if selected is None: | |
| return None, None | |
| item = standard_animation_lookup.get(selected) | |
| if item is None: | |
| raise gr.Error(f"Unknown Standard animation selection: {selected}") | |
| return item["path"], item["action"] | |
| def select_library_animation(selected: str): | |
| path, action = select2file(selected) | |
| return path, action, gr.Checkbox(value=True) | |
| select_animation_file.input( | |
| select_library_animation, | |
| inputs=select_animation_file, | |
| outputs=[input_animation_file, animation_action_state, input_reset_to_rest], | |
| preprocess=False, | |
| ) | |
| input_animation_file.upload( | |
| lambda: (gr.Dropdown(value=None), None, gr.Checkbox(value=True)), | |
| outputs=[select_animation_file, animation_action_state, input_reset_to_rest], | |
| ) | |
| input_animation_file.clear( | |
| lambda: (gr.Dropdown(value=None), None), | |
| outputs=[select_animation_file, animation_action_state], | |
| show_progress="hidden", | |
| )''', | |
| "Standard selection events", | |
| ) | |
| print("STATICAI Standard animation library integration patched.") | |