""" Bootstrap external repos + checkpoints when running on HF Gradio SDK Spaces (no Docker build phase to do it for us). In Docker mode the Dockerfile already cloned the wrapper + upstream repos and downloaded the baked checkpoints, so this module is a no-op there. On HF Gradio Spaces (or any clean OSS-local checkout where the user hasn't already provisioned external repos), we git-clone everything to a known base dir and call `huggingface_hub.snapshot_download` for the checkpoints. Auth: * Private GitHub repos via the `gh_token` env var (Space secret on HF; plain env var locally). * Private HF Hub repo via `hf_token` env var. Both never persisted to disk: the gh_token is used in a clone URL only during the clone, then stripped from the remote URL post-clone. Idempotent — safe to call multiple times. Skips any repo / file that already exists at the target path. """ from __future__ import annotations import logging import os import shutil import subprocess import sys from pathlib import Path log = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Config — overridable via env vars so OSS users can point this anywhere. # --------------------------------------------------------------------------- # Base dir for cloned external repos. On HF Gradio Spaces the working # directory is /home/user/app/, so external/ sits next to the app code. # In Docker mode the Dockerfile uses /opt/* and this whole module is a no-op # (the Docker-mode marker is COMFYUI_ANIMOFLOW_DIR pointing at /opt/...). _DEFAULT_EXTERNAL = ( "/home/user/app/external" if os.path.isdir("/home/user/app") else str(Path(__file__).resolve().parent / "external") ) EXTERNAL_DIR = Path(os.environ.get("ANIMOFLOW_EXTERNAL_DIR", _DEFAULT_EXTERNAL)) # Pinned upstream SHAs / revisions. Bump deliberately, not via git pull. # MDM repo HEAD diverged (BERT encoder, refactored encode_text → shape # changes, new imports) and breaks text-conditioned generation. Pin to the # last known-good commit that matches the checkpoint's architecture. MDM_PINNED_COMMIT = os.environ.get("MDM_PINNED_COMMIT", "af061ca") ANIMOFLOW_CHECKPOINTS_REPO = os.environ.get( "ANIMOFLOW_CHECKPOINTS_REPO", "AnimoFlow/animoflow-checkpoints" ) ANIMOFLOW_CHECKPOINTS_REVISION = os.environ.get( "ANIMOFLOW_CHECKPOINTS_REVISION", # 841707a = Pete removed; f0af610 = The Boss removed (2026-07-03, # Guy's call). Predecessor # cfbfe3c = first commit with characters/** (2026-07-03 webui character # bake — Mixamo FBXs live in this private repo, NOT in public # comfyui-animoflow; current roster: Vanguard/Knight/Suzie/Doozy). Predecessor # ff21939 was the first commit with priormdm/** (2026-06-21); its # predecessor 6df4521 had momask/t2m/** but no priorMDM, so # snapshot_download with allow_patterns=["priormdm/**"] globbed nothing # and the registry's _load("priormdm") failed cleanly at first call — # same bug class would hit characters/** on any pre-cfbfe3c revision. "841707a1584c2ffc6085fff44f5b00c555fab634", ) # Marker that says "Docker mode already provisioned everything" — if any of # these exist we skip the bootstrap entirely. _DOCKER_MODE_PATHS = ( "/opt/comfyui-animoflow", "/opt/animoflow-api", ) # What we need to clone. (repo_name, target_subdir, is_private). _GIT_REPOS: tuple[tuple[str, str, str, bool], ...] = ( # (full_url_template_or_url, target_subdir, friendly_name, private) ( "https://github.com/AnimoFlow/comfyui-animoflow.git", "comfyui-animoflow", "comfyui-animoflow", True, ), ( "https://github.com/AnimoFlow/animoflow-api.git", "animoflow-api", "animoflow-api", True, ), ( "https://github.com/GuyTevet/motion-diffusion-model.git", "mdm-codes", "mdm-codes", False, ), ( "https://github.com/EricGuo5513/momask-codes.git", "momask-codes", "momask-codes", False, ), ( "https://github.com/priorMDM/priorMDM.git", "priormdm-codes", "priormdm-codes", False, ), ) # Kimodo source repos cloned separately (not in _GIT_REPOS) so that the # ENABLE_KIMODO kill-switch can skip them entirely without affecting MDM/MoMask. # kimodo-viser is cloned NESTED inside kimodo-src/ to match the editable-install # layout the upstream Dockerfile expects (containers/kimodo/Dockerfile:22). _KIMODO_REPOS: tuple[tuple[str, str, str], ...] = ( ("https://github.com/nv-tlabs/kimodo.git", "kimodo-src", "kimodo-src"), ("https://github.com/nv-tlabs/kimodo-viser.git", "kimodo-src/kimodo-viser", "kimodo-viser"), ) def _docker_mode() -> bool: """Return True if the Dockerfile already provisioned the external repos.""" return any(Path(p).is_dir() for p in _DOCKER_MODE_PATHS) # Blender 5.0.1 portable Linux tarball. Replaces Debian Trixie's apt Blender # (4.3.2) — its retarget runtime was ~75x slower per stage timings. _BLENDER_VERSION = "5.0.1" _BLENDER_TARBALL_URL = ( f"https://download.blender.org/release/Blender5.0/" f"blender-{_BLENDER_VERSION}-linux-x64.tar.xz" ) _BLENDER_INSTALL_DIR = Path("/home/user/app") / f"blender-{_BLENDER_VERSION}-linux-x64" _BLENDER_BIN_PATH = _BLENDER_INSTALL_DIR / "blender" # gltfpack (meshoptimizer) — Draco + Meshopt GLB compression for Plan A. # Used by pipeline_hf._compress_glb. Pinning to a release tag (not SHA) because # upstream releases are stable artifacts — see [[preview-perf-2026-06-08 # -handoff]] gotcha 10. _GLTFPACK_VERSION = "1.1" _GLTFPACK_ZIP_URL = ( f"https://github.com/zeux/meshoptimizer/releases/download/" f"v{_GLTFPACK_VERSION}/gltfpack-ubuntu.zip" ) _GLTFPACK_INSTALL_DIR = Path("/home/user/app/bin") _GLTFPACK_BIN_PATH = _GLTFPACK_INSTALL_DIR / "gltfpack" def _install_gltfpack() -> None: """Install gltfpack (from meshoptimizer) for GLB compression. Used by pipeline_hf._compress_glb to apply Draco (geometry) + Meshopt (animation tracks + remaining buffers) compression to the GLB output. Validated 9.74x compression on a production Y_bot GLB (2.65 MB → 272 KB) pre-deployment. Per the no-silent-fallback rule ([[No silent fallback in dev mode]]): if the download or extract fails, raise. The Space must boot loudly on this kind of failure, not boot cleanly and only blow up on the first /generate call with a confusing "gltfpack not found" error. Idempotent — if the binary is already present and executable, just plant GLTFPACK_BIN and return. No-op in Docker mode (the container image is responsible for its own tooling). """ import time import urllib.request import zipfile if _docker_mode(): log.info("_install_gltfpack: docker mode, skip (container handles it)") return if _GLTFPACK_BIN_PATH.is_file() and os.access(_GLTFPACK_BIN_PATH, os.X_OK): os.environ["GLTFPACK_BIN"] = str(_GLTFPACK_BIN_PATH) log.info("_install_gltfpack: already installed at %s", _GLTFPACK_BIN_PATH) return log.info("_install_gltfpack: downloading gltfpack %s from %s", _GLTFPACK_VERSION, _GLTFPACK_ZIP_URL) t0 = time.perf_counter() zip_path = Path("/tmp") / f"gltfpack-{_GLTFPACK_VERSION}.zip" # GitHub release downloads work with default Python urllib UA, but # mirror the Blender installer's defensive Mozilla UA — cheap insurance # if Cloudflare ever changes its mind about default Python clients. req = urllib.request.Request( _GLTFPACK_ZIP_URL, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"}, ) with urllib.request.urlopen(req, timeout=120) as src, open(zip_path, "wb") as dst: shutil.copyfileobj(src, dst) log.info("_install_gltfpack: downloaded in %.1fs (%d KB)", time.perf_counter() - t0, zip_path.stat().st_size // 1024) _GLTFPACK_INSTALL_DIR.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(zip_path, "r") as zf: zf.extractall(_GLTFPACK_INSTALL_DIR) try: zip_path.unlink() except OSError: pass if not _GLTFPACK_BIN_PATH.is_file(): raise RuntimeError( f"_install_gltfpack: expected binary at {_GLTFPACK_BIN_PATH} after " f"extracting {_GLTFPACK_ZIP_URL} — upstream asset layout may have " f"changed; check `gh release view v{_GLTFPACK_VERSION} -R " f"zeux/meshoptimizer --json assets`" ) os.chmod(_GLTFPACK_BIN_PATH, 0o755) os.environ["GLTFPACK_BIN"] = str(_GLTFPACK_BIN_PATH) log.info("_install_gltfpack: installed at %s (%d KB)", _GLTFPACK_BIN_PATH, _GLTFPACK_BIN_PATH.stat().st_size // 1024) def _install_blender_portable() -> None: """Install Blender 5.0.1 portable Linux tarball and point BLENDER_BIN at it. Replaces Debian Trixie's apt Blender 4.3.2 which is ~75x slower at the BVH→FBX retarget step per the Space's [STAGE_TIMINGS] (~90s/job vs ~1.2s on Mac native Blender 5.0.1). Same pattern previously used on the seoul GPU render machine. Idempotent: if the binary already exists at the install path, just sets BLENDER_BIN and returns. No-op in Docker mode (the container's Dockerfile.cpu already pins its own Blender). """ import subprocess as _subproc import tarfile import time import urllib.request if _docker_mode(): log.info("_install_blender_portable: docker mode, skip (container handles Blender)") return # Idempotency check — survives Space restarts when /home/user/app persists if _BLENDER_BIN_PATH.is_file(): os.environ["BLENDER_BIN"] = str(_BLENDER_BIN_PATH) log.info("_install_blender_portable: already installed at %s", _BLENDER_BIN_PATH) return log.info("_install_blender_portable: downloading Blender %s (~360 MB) from %s", _BLENDER_VERSION, _BLENDER_TARBALL_URL) t0 = time.perf_counter() tarball_path = Path("/tmp") / f"blender-{_BLENDER_VERSION}-linux-x64.tar.xz" # download.blender.org is Cloudflare-fronted and 403s on default Python UA. req = urllib.request.Request( _BLENDER_TARBALL_URL, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"}, ) try: with urllib.request.urlopen(req, timeout=300) as src, open(tarball_path, "wb") as dst: while True: chunk = src.read(1024 * 1024) if not chunk: break dst.write(chunk) except Exception as e: log.warning("_install_blender_portable: download FAILED (%s) — falling back to apt Blender", e) return log.info("_install_blender_portable: downloaded in %.1fs (%d MB)", time.perf_counter() - t0, tarball_path.stat().st_size // (1024 * 1024)) t1 = time.perf_counter() install_parent = _BLENDER_INSTALL_DIR.parent install_parent.mkdir(parents=True, exist_ok=True) try: with tarfile.open(tarball_path, "r:xz") as tar: tar.extractall(install_parent) except Exception as e: log.warning("_install_blender_portable: extract FAILED (%s) — falling back to apt Blender", e) return try: tarball_path.unlink() except Exception: pass log.info("_install_blender_portable: extracted in %.1fs to %s", time.perf_counter() - t1, _BLENDER_INSTALL_DIR) if not _BLENDER_BIN_PATH.is_file(): log.warning("_install_blender_portable: binary missing at %s after extract — falling back to apt Blender", _BLENDER_BIN_PATH) return os.environ["BLENDER_BIN"] = str(_BLENDER_BIN_PATH) # Smoke test try: ver = _subproc.run( [str(_BLENDER_BIN_PATH), "--version"], capture_output=True, text=True, timeout=30, ) first_line = (ver.stdout or "").splitlines()[0] if ver.stdout else "" log.info("_install_blender_portable: BLENDER_BIN=%s | %s", _BLENDER_BIN_PATH, first_line or "") except Exception as e: log.warning("_install_blender_portable: version smoke failed (%s) but BLENDER_BIN set anyway", e) # --------------------------------------------------------------------------- # Kimodo escape-hatch venv builder # --------------------------------------------------------------------------- # Kimodo's NVIDIA stack pins transformers==5.1.0 + py-soma-x + warp-lang etc. # which fight the MDM/MoMask pins in the orchestrator venv. We isolate it in # /home/user/app/venvs/kimodo/ and invoke via subprocess from escape_hatch. # Build runs in a background thread so the Gradio UI comes up promptly and # MDM/MoMask stay usable while Kimodo warms (~5-10 min cold install: pip deps # + 16 GB LLaMA-3-8B encoder download). # # Sentinels in EXTERNAL_DIR: # .kimodo_ready — venv built, weights cached. Set on success. # .kimodo_failed — captured error text. Set on any failure during install. # # Per [[No silent fallback in dev mode]]: any failure raises loudly and writes # .kimodo_failed; the runner script + escape_hatch.invoke surface it to the UI. _KIMODO_VENV_DIR = Path("/home/user/app/venvs/kimodo") _KIMODO_BUILD_THREAD: "threading.Thread | None" = None _KIMODO_BUILD_EVENT: "threading.Event | None" = None # set when ready def _kimodo_ready_sentinel() -> Path: return EXTERNAL_DIR / ".kimodo_ready" def _kimodo_failed_sentinel() -> Path: return EXTERNAL_DIR / ".kimodo_failed" def _kimodo_text_encoders_dir() -> Path: return EXTERNAL_DIR / "text-encoders" def _kimodo_assets_dir() -> Path: return EXTERNAL_DIR / "kimodo-assets" def _kimodo_src_dir() -> Path: return EXTERNAL_DIR / "kimodo-src" def _clone_kimodo_repos() -> None: """Clone Kimodo + kimodo-viser. Idempotent. Skipped via ENABLE_KIMODO=false.""" EXTERNAL_DIR.mkdir(parents=True, exist_ok=True) for url, subdir, name in _KIMODO_REPOS: target = EXTERNAL_DIR / subdir if target.is_dir(): log.info("kimodo-clone: %s already at %s — skip", name, target) continue log.info("kimodo-clone (public): %s → %s", name, target) _git_clone(url, target) def _patch_kimodo_requirements() -> None: """Strip MotionCorrection from Kimodo's lockfile (C++ post-processor that needs cmake + CUDA; we don't use post-processing). Mirrors the Docker Dockerfile RUN sed -i '/MotionCorrection/d' line. Idempotent.""" req_path = _kimodo_src_dir() / "docker_requirements.txt" if not req_path.is_file(): log.warning("_patch_kimodo_requirements: %s not found, skipping", req_path) return text = req_path.read_text() if "MotionCorrection" not in text: log.info("_patch_kimodo_requirements: already stripped — skip") return new_lines = [ln for ln in text.splitlines(keepends=True) if "MotionCorrection" not in ln] req_path.write_text("".join(new_lines)) log.info("_patch_kimodo_requirements: stripped MotionCorrection from %s", req_path) def _kimodo_venv_python() -> Path: return _KIMODO_VENV_DIR / "bin" / "python" def _ensure_kimodo_venv() -> None: """Create the venv with stdlib `venv`. Idempotent.""" if _kimodo_venv_python().is_file(): log.info("_ensure_kimodo_venv: venv already at %s — skip", _KIMODO_VENV_DIR) return log.info("_ensure_kimodo_venv: creating venv at %s", _KIMODO_VENV_DIR) _KIMODO_VENV_DIR.parent.mkdir(parents=True, exist_ok=True) subprocess.run( [sys.executable, "-m", "venv", str(_KIMODO_VENV_DIR)], check=True, capture_output=True, text=True, ) def _kimodo_pip(*args: str, env_extra: dict | None = None, cwd: str | None = None) -> None: """Run pip inside the Kimodo venv. Raises on non-zero. ``cwd`` is required when the lockfile contains relative editable paths like ``-e ./kimodo-viser`` (Kimodo's ``docker_requirements.txt`` does this). The upstream Dockerfile changes directory before running pip; mirror that. """ cmd = [str(_kimodo_venv_python()), "-m", "pip", *args] log.info( "kimodo-pip%s: %s", f" (cwd={cwd})" if cwd else "", " ".join(args[:3]) + (" …" if len(args) > 3 else ""), ) env = {**os.environ, **(env_extra or {})} proc = subprocess.run(cmd, capture_output=True, text=True, env=env, cwd=cwd) if proc.returncode != 0: # Surface tail of stderr AND stdout (pip writes most of its useful # diagnostics to stdout, not stderr) so the .kimodo_failed sentinel # captures the actionable detail. 4000 chars each is generous but # the sentinel is one-shot per build attempt. raise RuntimeError( f"kimodo-pip failed (rc={proc.returncode}): {' '.join(args[:8])}\n" f"stdout (last 4000 chars): {proc.stdout[-4000:]}\n" f"stderr (last 4000 chars): {proc.stderr[-4000:]}" ) def _install_kimodo_deps() -> None: """Install Kimodo's pinned lockfile into the Kimodo venv.""" req = _kimodo_src_dir() / "docker_requirements.txt" if not req.is_file(): raise RuntimeError(f"Kimodo docker_requirements.txt missing at {req}") # Upgrade pip + install wheel/setuptools first. The Kimodo Docker image # is based on nvcr.io/nvidia/pytorch which ships these; our stdlib venv # does not. Without `wheel`, chumpy 0.70 (in _extract_j_regressor below) # fails at metadata generation with `invalid command 'bdist_wheel'`. _kimodo_pip("install", "--upgrade", "pip", "setuptools", "wheel") # docker_requirements.txt contains `-e ./kimodo-viser` (relative editable # install). pip resolves it against the current working directory, so we # must `cd` into kimodo-src — matching containers/kimodo/Dockerfile L30 # (`RUN cd /workspace/kimodo-src && pip install -r docker_requirements.txt`). # The lockfile also triggers py-soma-x's setup.py which conditionally # imports MotionCorrection if SKIP_MOTION_CORRECTION_IN_SETUP is unset. _kimodo_pip( "install", "-r", "docker_requirements.txt", env_extra={"SKIP_MOTION_CORRECTION_IN_SETUP": "1"}, cwd=str(_kimodo_src_dir()), ) def _prefetch_llama_encoder(hf_token: str | None) -> None: """Pre-fetch LLaMA-3-8B (NousResearch ungated mirror) into the text-encoders dir so first inference doesn't pay the ~16 GB download.""" # Use the ORCHESTRATOR's huggingface_hub (cheaper than spawning the Kimodo # venv just to download). Both venvs see the same EXTERNAL_DIR mount. from huggingface_hub import snapshot_download target = _kimodo_text_encoders_dir() / "NousResearch" / "Meta-Llama-3-8B-Instruct" if (target / "config.json").is_file(): log.info("_prefetch_llama_encoder: already at %s — skip", target) return target.parent.mkdir(parents=True, exist_ok=True) log.info("_prefetch_llama_encoder: downloading ~16 GB to %s", target) snapshot_download( repo_id="NousResearch/Meta-Llama-3-8B-Instruct", local_dir=str(target), token=hf_token, # works with or without token (this repo is ungated) ) def _prefetch_soma_x_assets(hf_token: str | None) -> None: """Pre-fetch nvidia/soma-x assets into the standard HF Hub cache. Kimodo's _get_mesh_pipeline expects base_body.obj + SMPL skin data under `~/.cache/huggingface/hub/models--nvidia--soma-x/snapshots/*/SMPL/`. Neither `kimodo.load_model` nor any obvious upstream code path triggers this download — the Docker container apparently relied on a side-effect we no longer have. Pre-fetch explicitly so the first inference works. Uses the standard HF Hub cache (not local_dir) so the existing glob in containers/kimodo/app.py finds it without env-var plumbing on the runner. """ from huggingface_hub import snapshot_download # Marker file we'd see post-download. If present, skip. import glob as _glob hf_cache_roots = [ os.environ.get("HF_HOME", "").strip(), os.environ.get("HUGGINGFACE_HUB_CACHE", "").strip(), os.path.expanduser("~/.cache/huggingface"), ] for root in hf_cache_roots: if not root: continue if _glob.glob(os.path.join(root, "hub", "models--nvidia--soma-x", "snapshots", "*", "SMPL", "base_body.obj")): log.info("_prefetch_soma_x_assets: already cached under %s — skip", root) return log.info("_prefetch_soma_x_assets: downloading nvidia/soma-x to HF cache") snapshot_download(repo_id="nvidia/soma-x", token=hf_token) def _patch_llm2vec_configs() -> None: """Rewrite LLM2Vec adapter configs to point at the ungated LLaMA mirror. Reuses comfyui-animoflow/containers/kimodo/app.py:_patch_llm2vec_for_ungated_llama by importing it as a plain function (no FastAPI app served).""" container_kimodo = ( Path(os.environ.get("COMFYUI_ANIMOFLOW_DIR", str(EXTERNAL_DIR / "comfyui-animoflow"))) / "containers" / "kimodo" ) app_py = container_kimodo / "app.py" if not app_py.is_file(): raise RuntimeError( f"_patch_llm2vec_configs: helper not found at {app_py}. " "comfyui-animoflow must be cloned before this step." ) # Plant TEXT_ENCODERS_DIR + HF_HOME so the helper writes to our paths. os.environ["TEXT_ENCODERS_DIR"] = str(_kimodo_text_encoders_dir()) # Import-via-spec so the module's top-level FastAPI construction doesn't # require Kimodo deps in the orchestrator venv (the helper itself only # needs huggingface_hub which we have). import importlib.util as _ilu spec = _ilu.spec_from_file_location("_kimodo_helpers", app_py) mod = _ilu.module_from_spec(spec) # type: ignore[arg-type] # The helper imports torch + fastapi at module top. To dodge those when # we only want the config rewrite, monkey-load the source and exec only # the function we need. source = app_py.read_text() # Extract just the _patch_llm2vec_for_ungated_llama function by string # slicing — small, stable surface. Falls back to full module exec if the # markers move (with a clearer error than NameError later). start = source.find("def _patch_llm2vec_for_ungated_llama") if start < 0: raise RuntimeError( "_patch_llm2vec_configs: marker 'def _patch_llm2vec_for_ungated_llama' " f"not found in {app_py}. Source layout changed; update bootstrap." ) # Find the end of the function — next top-level def or end of file. end_markers = ("\ndef ", "\nclass ", "\n# -") end = len(source) for m in end_markers: idx = source.find(m, start + 1) if idx > 0: end = min(end, idx) fn_source = source[start:end] # The function also references module-level constants we need to plant. preamble = ( "import json, os\n" "from pathlib import Path\n" "from huggingface_hub import snapshot_download\n" f"_ENCODERS_DIR = {str(_kimodo_text_encoders_dir())!r}\n" "_UNGATED_LLAMA = 'NousResearch/Meta-Llama-3-8B-Instruct'\n" "_LLM2VEC_ADAPTERS = [\n" " 'McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp',\n" " 'McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised',\n" "]\n" ) ns: dict = {} exec(preamble + fn_source, ns) # noqa: S102 — controlled source from our own repo ns["_patch_llm2vec_for_ungated_llama"]() log.info("_patch_llm2vec_configs: LLM2Vec adapter configs rewired to ungated LLaMA") def _extract_j_regressor() -> None: """Build J_regressor_22.npy from SMPL_NEUTRAL.pkl. Idempotent. Mirrors containers/kimodo/Dockerfile lines 43-50: - install chumpy + gdown in the kimodo venv (chumpy needs --no-build-isolation) - gdown SMPL_NEUTRAL.pkl - run extract_j_regressor.py - clean up """ assets = _kimodo_assets_dir() assets.mkdir(parents=True, exist_ok=True) j_reg_path = assets / "J_regressor_22.npy" if j_reg_path.is_file(): log.info("_extract_j_regressor: already at %s — skip", j_reg_path) return smpl_pkl = assets / "SMPL_NEUTRAL.pkl" extract_script = ( Path(os.environ.get("COMFYUI_ANIMOFLOW_DIR", str(EXTERNAL_DIR / "comfyui-animoflow"))) / "containers" / "kimodo" / "extract_j_regressor.py" ) if not extract_script.is_file(): raise RuntimeError(f"_extract_j_regressor: script missing at {extract_script}") try: # Mirror containers/kimodo/Dockerfile L43-50: chumpy needs # --no-build-isolation (its setup.py imports pip internals). gdown # is a small modern package and installs cleanly without the flag. # Installing them separately also makes which-one-failed obvious. _kimodo_pip("install", "--no-build-isolation", "chumpy") _kimodo_pip("install", "gdown") # Download SMPL_NEUTRAL.pkl via gdown (drive ID from container Dockerfile). subprocess.run( [ str(_kimodo_venv_python()), "-c", "import gdown; gdown.download(" "'https://drive.google.com/uc?id=1INYlGA76ak_cKGzvpOV2Pe6RkYTlXTW2'," f"'{smpl_pkl}'," "quiet=False)", ], check=True, capture_output=True, text=True, ) # Run the extractor: SMPL_NEUTRAL.pkl → J_regressor_22.npy subprocess.run( [str(_kimodo_venv_python()), str(extract_script), str(smpl_pkl), str(j_reg_path)], check=True, capture_output=True, text=True, ) finally: # Clean up the gated SMPL pkl + chumpy regardless of success. try: if smpl_pkl.is_file(): smpl_pkl.unlink() except OSError: pass try: _kimodo_pip("uninstall", "-y", "chumpy", "gdown") except Exception: # noqa: BLE001 log.warning("_extract_j_regressor: cleanup uninstall failed (non-fatal)") if not j_reg_path.is_file(): raise RuntimeError(f"_extract_j_regressor: {j_reg_path} not produced") log.info("_extract_j_regressor: %s built (%d bytes)", j_reg_path, j_reg_path.stat().st_size) def _audit_disk() -> None: """Loud warning if /home/user/app is approaching the HF Space quota.""" try: proc = subprocess.run( ["du", "-sh", "/home/user/app"], capture_output=True, text=True, timeout=30, ) log.info("kimodo-bootstrap: disk usage %s", proc.stdout.strip()) except Exception as e: # noqa: BLE001 log.warning("kimodo-bootstrap: disk audit failed: %s", e) def _install_kimodo_sync(hf_token: str | None) -> None: """Synchronous Kimodo venv build. Called from a background thread by _install_kimodo_async. Sets .kimodo_ready on success or .kimodo_failed on any error, both consumed by run_inference_kimodo.py + escape_hatch.invoke. """ import time t0 = time.perf_counter() failed_sentinel = _kimodo_failed_sentinel() ready_sentinel = _kimodo_ready_sentinel() # Clear stale sentinels from previous attempts. for s in (failed_sentinel, ready_sentinel): try: s.unlink() except FileNotFoundError: pass try: log.info("[kimodo-bootstrap] starting") _clone_kimodo_repos() _patch_kimodo_requirements() _ensure_kimodo_venv() _install_kimodo_deps() _prefetch_llama_encoder(hf_token) _patch_llm2vec_configs() _prefetch_soma_x_assets(hf_token) _extract_j_regressor() _audit_disk() ready_sentinel.write_text("ok\n") if _KIMODO_BUILD_EVENT is not None: _KIMODO_BUILD_EVENT.set() log.info( "[kimodo-bootstrap] ready in %.0fs (venv=%s, ready=%s)", time.perf_counter() - t0, _KIMODO_VENV_DIR, ready_sentinel, ) except Exception as exc: # noqa: BLE001 — capture everything so UI can show it import traceback msg = f"{type(exc).__name__}: {exc}\n\n{traceback.format_exc()}" try: failed_sentinel.write_text(msg) except Exception: pass log.exception("[kimodo-bootstrap] FAILED after %.0fs", time.perf_counter() - t0) # Don't re-raise: background thread exits, but the sentinel + UI surface # the error. escape_hatch.invoke reads .kimodo_failed and refuses to # call into a broken venv. def _install_kimodo_async(hf_token: str | None) -> None: """Kick off Kimodo build in a daemon thread. Returns immediately. Calling again is a no-op if the build is already running, ready, or failed.""" import threading as _threading global _KIMODO_BUILD_THREAD, _KIMODO_BUILD_EVENT if _KIMODO_BUILD_EVENT is None: _KIMODO_BUILD_EVENT = _threading.Event() # Expose the event on escape_hatch.invoke for the readiness gate. # NOTE: escape_hatch/__init__.py does `from .invoke import invoke` which # overwrites the submodule reference with the function — even # `import escape_hatch.invoke as X` then binds X to the function. Pull # the module from sys.modules to bypass the shadowing. try: import escape_hatch # noqa: F401 — ensures escape_hatch.invoke is loaded import sys as _sys _ehi = _sys.modules["escape_hatch.invoke"] _ehi._KIMODO_READY = _KIMODO_BUILD_EVENT # type: ignore[attr-defined] except Exception: # noqa: BLE001 # escape_hatch import may fail during early bootstrap; the runner # also reads the sentinel file as a backup. pass # Fast paths: nothing to do. if _kimodo_ready_sentinel().is_file(): if not _KIMODO_BUILD_EVENT.is_set(): _KIMODO_BUILD_EVENT.set() log.info("_install_kimodo_async: .kimodo_ready already present — skip") return if _KIMODO_BUILD_THREAD is not None and _KIMODO_BUILD_THREAD.is_alive(): log.info("_install_kimodo_async: build already in progress — skip") return if _kimodo_failed_sentinel().is_file(): log.warning( "_install_kimodo_async: previous build FAILED (see %s). " "Delete the sentinel + restart to retry.", _kimodo_failed_sentinel(), ) return log.info("_install_kimodo_async: spawning build thread") _KIMODO_BUILD_THREAD = _threading.Thread( target=_install_kimodo_sync, args=(hf_token,), daemon=True, name="kimodo-bootstrap", ) _KIMODO_BUILD_THREAD.start() def _set_env_for_kimodo() -> None: """Plant env vars the runner script + escape_hatch + animoflow-api read to find the Kimodo venv, runner, assets, and health endpoint.""" os.environ.setdefault("KIMODO_VENV_PYTHON", str(_kimodo_venv_python())) os.environ.setdefault( "KIMODO_RUNNER_SCRIPT", str(Path(__file__).resolve().parent / "scripts" / "run_inference_kimodo.py"), ) os.environ.setdefault("KIMODO_SRC_DIR", str(_kimodo_src_dir())) os.environ.setdefault("KIMODO_ASSETS_DIR", str(_kimodo_assets_dir())) os.environ.setdefault("TEXT_ENCODERS_DIR", str(_kimodo_text_encoders_dir())) os.environ.setdefault( "J_REGRESSOR_PATH", str(_kimodo_assets_dir() / "J_regressor_22.npy"), ) # Health endpoint: animoflow-api's _MODEL_HEALTH_URLS polls # ${KIMODO_ENDPOINT}/health. We expose /__internal/kimodo_health on the # same FastAPI app — see app.py. port = os.environ.get("PORT", "7860") os.environ.setdefault( "KIMODO_ENDPOINT", f"http://127.0.0.1:{port}/__internal/kimodo", ) def _git_clone(url: str, target: Path, *, depth: int = 1) -> None: """Shell out to `git clone --depth N`. Raises CalledProcessError on failure.""" subprocess.run( ["git", "clone", f"--depth={depth}", url, str(target)], check=True, capture_output=True, text=True, ) def _strip_remote_auth(target: Path, clean_url: str) -> None: """Replace the origin URL with the unauthenticated URL post-clone, so the token isn't persisted on disk in `.git/config`.""" subprocess.run( ["git", "-C", str(target), "remote", "set-url", "origin", clean_url], check=True, capture_output=True, text=True, ) def _clone_all(gh_token: str | None) -> None: EXTERNAL_DIR.mkdir(parents=True, exist_ok=True) for url, subdir, name, private in _GIT_REPOS: target = EXTERNAL_DIR / subdir if target.is_dir(): log.info("clone: %s already at %s — skip", name, target) continue if private: if not gh_token: raise RuntimeError( f"gh_token env var missing — cannot clone private repo {name}. " "On HF Spaces, configure a Space secret named `gh_token` with " "Read access to AnimoFlow private repos." ) auth_url = url.replace("https://", f"https://x-access-token:{gh_token}@") log.info("clone (private): %s → %s", name, target) _git_clone(auth_url, target) _strip_remote_auth(target, url) else: log.info("clone (public): %s → %s", name, target) _git_clone(url, target) # Pin MDM to a known-good commit (HEAD diverged, see MDM_PINNED_COMMIT). if subdir == "mdm-codes" and MDM_PINNED_COMMIT: _pin_to_commit(target, MDM_PINNED_COMMIT, name) def _pin_to_commit(target: Path, commit: str, name: str) -> None: """Fetch a specific commit and checkout, so the clone stays at a known-good revision regardless of what HEAD points to. GitHub doesn't allow fetching arbitrary SHAs by default on shallow clones. Strategy: try shallow fetch first (works on some hosts); if that fails, unshallow the repo and then checkout. """ try: # Fast path: try fetching the SHA directly (works on GH if # uploadpack.allowReachableSHA1InWant is enabled). subprocess.run( ["git", "-C", str(target), "fetch", "--depth=1", "origin", commit], check=True, capture_output=True, text=True, ) except subprocess.CalledProcessError: # Slow path: unshallow so the commit is reachable, then checkout. log.info("pin: shallow fetch of %s failed for %s — unshallowing", commit, name) try: subprocess.run( ["git", "-C", str(target), "fetch", "--unshallow"], check=True, capture_output=True, text=True, ) except subprocess.CalledProcessError: # Already unshallow or fetch failed — try a plain fetch subprocess.run( ["git", "-C", str(target), "fetch", "origin"], check=True, capture_output=True, text=True, ) try: subprocess.run( ["git", "-C", str(target), "checkout", commit], check=True, capture_output=True, text=True, ) log.info("pin: %s checked out at %s", name, commit) except subprocess.CalledProcessError as exc: log.warning("pin: failed to checkout %s at %s: %s", name, commit, exc.stderr) def _download_checkpoints(hf_token: str | None) -> Path | None: """Download MDM checkpoints from HF Hub and return the local dir, or None if no token (caller decides whether that's fatal).""" ckpt_dir = EXTERNAL_DIR / "checkpoints" ckpt_dir.mkdir(parents=True, exist_ok=True) target_pt = ckpt_dir / "humanml_enc_512_50steps" / "model000750000.pt" if target_pt.exists(): log.info("checkpoints: already present at %s", ckpt_dir) return ckpt_dir if not hf_token: log.warning( "hf_token env var missing — MDM checkpoints not downloaded. " "MDM will fall back to placeholder mode. Configure `hf_token` " "Space secret with Read access to %s.", ANIMOFLOW_CHECKPOINTS_REPO, ) return None from huggingface_hub import snapshot_download log.info( "checkpoints: snapshot_download %s @ %s → %s", ANIMOFLOW_CHECKPOINTS_REPO, ANIMOFLOW_CHECKPOINTS_REVISION[:8], ckpt_dir, ) snapshot_download( repo_id=ANIMOFLOW_CHECKPOINTS_REPO, revision=ANIMOFLOW_CHECKPOINTS_REVISION, repo_type="model", local_dir=str(ckpt_dir), allow_patterns=[ "humanml_enc_512_50steps/model000750000.pt", "humanml_enc_512_50steps/args.json", # MoMask T2M checkpoints — required by inference_momask.py which # reads `${CHECKPOINTS_DIR}/t2m/{rvq,t2m,tres,length_estimator}/...`. # We override CHECKPOINTS_DIR to `${ckpt_dir}/momask` per-model in # registry._load() so this path becomes `${ckpt_dir}/momask/t2m/...`. "momask/t2m/**", # priorMDM trajectory + timeline heads. The registry's # env_overrides_factory plants CHECKPOINT_DIR=${ckpt_dir}/priormdm # so inference.py's _discover_checkpoint() looks inside # priormdm/{root_horizontal_control_50steps,humanml-encoder-512-50steps}/. "priormdm/**", ], token=hf_token, ) # Copy t2m_mean / t2m_std from the comfyui-animoflow clone — the # MDM wrapper expects them in WEIGHTS_DIR alongside the .pt. src_dir = EXTERNAL_DIR / "comfyui-animoflow" / "containers" / "mdm" for npy in ("t2m_mean.npy", "t2m_std.npy"): src = src_dir / npy dst = ckpt_dir / npy if src.exists() and not dst.exists(): shutil.copyfile(src, dst) log.info("copied %s → %s", src, dst) # Mirror for priorMDM: its inference.py reads mean/std from WEIGHTS_DIR # which the registry sets to ${ckpt_dir}/priormdm. Same files shipped in # the priormdm container — copy them so the path resolves. pmdm_src_dir = EXTERNAL_DIR / "comfyui-animoflow" / "containers" / "priormdm" pmdm_dst_dir = ckpt_dir / "priormdm" if pmdm_src_dir.exists(): pmdm_dst_dir.mkdir(parents=True, exist_ok=True) for npy in ("t2m_mean.npy", "t2m_std.npy"): src = pmdm_src_dir / npy dst = pmdm_dst_dir / npy if src.exists() and not dst.exists(): shutil.copyfile(src, dst) log.info("copied %s → %s", src, dst) return ckpt_dir def _download_private_characters(hf_token: str | None) -> None: """Fetch the Mixamo character FBXs from the private checkpoints repo into the cloned comfyui-animoflow characters dir. The public comfyui-animoflow repo only carries download instructions (Mixamo terms forbid redistribution) plus the small bone_map.json sidecars; the hosted demo gets the actual FBXs from AnimoFlow/animoflow-checkpoints `characters/**`. Idempotent: skips files already present. No token → loud warning, characters absent → /v1/characters only lists whatever the public clone carries (Y_bot, Kaya, Cartoon_Character). """ chars_dir = EXTERNAL_DIR / "comfyui-animoflow" / "characters" if not chars_dir.is_dir(): log.warning("characters: %s missing — clone step failed?", chars_dir) return if not hf_token: log.warning( "hf_token env var missing — private characters not downloaded; " "the character dropdown will only show the public-clone rigs." ) return from huggingface_hub import snapshot_download log.info( "characters: snapshot_download %s @ %s characters/** → %s", ANIMOFLOW_CHECKPOINTS_REPO, ANIMOFLOW_CHECKPOINTS_REVISION[:8], chars_dir, ) import tempfile with tempfile.TemporaryDirectory() as tmp: snapshot_download( repo_id=ANIMOFLOW_CHECKPOINTS_REPO, revision=ANIMOFLOW_CHECKPOINTS_REVISION, repo_type="model", token=hf_token, local_dir=tmp, allow_patterns=["characters/**"], ) src_dir = Path(tmp) / "characters" n = 0 if src_dir.is_dir(): for f in sorted(src_dir.iterdir()): if not f.is_file(): continue dst = chars_dir / f.name if dst.exists() and dst.stat().st_size == f.stat().st_size: continue shutil.copy2(f, dst) n += 1 log.info("characters: installed %d file(s) into %s", n, chars_dir) def _set_env_for_external() -> None: """Plant env vars that the rest of the orchestrator code reads, so pipeline_hf, models.registry, etc. find the cloned paths without knowing whether we're in Docker or Gradio mode.""" os.environ.setdefault( "COMFYUI_ANIMOFLOW_DIR", str(EXTERNAL_DIR / "comfyui-animoflow") ) os.environ.setdefault( "COMFYUI_ANIMOFLOW_NODES_DIR", str(EXTERNAL_DIR / "comfyui-animoflow" / "nodes"), ) os.environ.setdefault( "ANIMOFLOW_API_API_DIR", str(EXTERNAL_DIR / "animoflow-api" / "api") ) os.environ.setdefault("MDM_PATH", str(EXTERNAL_DIR / "mdm-codes")) os.environ.setdefault("MOMASK_PATH", str(EXTERNAL_DIR / "momask-codes")) os.environ.setdefault("PRIORMDM_PATH", str(EXTERNAL_DIR / "priormdm-codes")) os.environ.setdefault( "CHARACTERS_DIR", str(EXTERNAL_DIR / "comfyui-animoflow" / "characters"), ) os.environ.setdefault("WEB_DIR", "/nonexistent") os.environ.setdefault("WEIGHTS_DIR", str(EXTERNAL_DIR / "checkpoints")) os.environ.setdefault("CHECKPOINTS_DIR", str(EXTERNAL_DIR / "checkpoints")) def _patch_mdm_model_util(): """Ensure MDM's utils/model_util.py has a ``load_saved_model`` function. The MDM inference wrapper (comfyui-animoflow/containers/mdm/inference.py) imports ``load_saved_model`` from ``utils.model_util``, but the upstream MDM repo only exposes ``load_model_wo_clip``. Without this shim the import fails silently (caught by a broad except), and the model falls back to a hardcoded walk-cycle placeholder — ignoring the text prompt entirely. This patch adds a thin ``load_saved_model(model, path, **kw)`` wrapper that loads the state dict from *path* and delegates to ``load_model_wo_clip``. Idempotent — skips if the function already exists in the file. """ mdm_path = EXTERNAL_DIR / "mdm-codes" model_util = mdm_path / "utils" / "model_util.py" if not model_util.is_file(): log.info("_patch_mdm_model_util: %s not found, skipping", model_util) return text = model_util.read_text() if "def load_saved_model" in text: log.info("_patch_mdm_model_util: load_saved_model already present — skip") return shim = ''' def load_saved_model(model, model_path, use_avg=False, **kwargs): """Compatibility shim: load checkpoint and delegate to load_model_wo_clip. The checkpoint stores only the non-CLIP weights (CLIP weights are stripped at training-time save). ``load_model_wo_clip`` loads them with ``strict=False`` so the freshly-downloaded CLIP weights in the model are preserved. """ import torch as _torch state_dict = _torch.load(model_path, map_location="cpu") load_model_wo_clip(model, state_dict) ''' model_util.write_text(text + shim) log.info("_patch_mdm_model_util: injected load_saved_model shim into %s", model_util) def _patch_priormdm_source() -> None: """Mirror comfyui-animoflow/containers/priormdm/Dockerfile lines 46-53. priorMDM's upstream source still uses numpy 1.x aliases (np.float, np.int, np.bool) removed in numpy 1.24+, and asserts dataset size > 1 which our single-sample inference path violates. The container's Dockerfile sed-fixes these at image build time; we mirror them here because we run priorMDM inside the orchestrator venv (numpy 2.x), not the container's 1.23 pin. Idempotent via a sentinel file. Safe to re-run after each bootstrap. """ import re src = EXTERNAL_DIR / "priormdm-codes" if not src.is_dir(): log.info("_patch_priormdm_source: %s not found, skipping", src) return sentinel = src / ".animoflow_patched" if sentinel.is_file(): log.info("_patch_priormdm_source: already patched (%s) — skip", sentinel) return np_alias_subs = [ (re.compile(r"\bnp\.float\b"), "float"), (re.compile(r"\bnp\.int\b"), "int"), (re.compile(r"\bnp\.bool\b"), "bool"), ] touched = 0 for py in src.rglob("*.py"): text = py.read_text() new = text for pat, repl in np_alias_subs: new = pat.sub(repl, new) if new != text: py.write_text(new) touched += 1 dset = src / "data_loaders" / "humanml" / "data" / "dataset.py" if dset.is_file(): t = dset.read_text() t2 = t.replace( "assert len(self.t2m_dataset) > 1", "assert len(self.t2m_dataset) >= 1", ) if t2 != t: dset.write_text(t2) sentinel.write_text("ok\n") log.info( "_patch_priormdm_source: patched %d .py files + dataset assert in %s", touched, src, ) def _patch_blender_bvh_addon(): """Fix Blender's io_anim_bvh addon: 'rU' mode removed in Python 3.12. Strategy: create a full copy of Blender's scripts directory at /home/user/app/blender_scripts_patched, fix the BVH addon, and set BLENDER_SYSTEM_SCRIPTS to redirect Blender there. """ import shutil sys_scripts = Path("/usr/share/blender/scripts") if not sys_scripts.is_dir(): log.info("_patch_blender_bvh_addon: /usr/share/blender/scripts not found, skipping") return target_file = sys_scripts / "addons/io_anim_bvh/import_bvh.py" if not target_file.is_file(): log.info("_patch_blender_bvh_addon: %s not found, skipping", target_file) return # Check if system file has the bug sys_text = target_file.read_text() if "'rU'" not in sys_text and '"rU"' not in sys_text: log.info("_patch_blender_bvh_addon: no 'rU' found in system file, already clean") return # Try direct fix first (might work if container allows it) try: fixed = sys_text.replace("'rU'", "'r'").replace('"rU"', '"r"') target_file.write_text(fixed) # Verify if "'rU'" not in target_file.read_text(): log.info("_patch_blender_bvh_addon: patched system file directly at %s", target_file) return except PermissionError: log.info("_patch_blender_bvh_addon: no write access to system file, using BLENDER_SYSTEM_SCRIPTS override") # Fallback: copy entire scripts dir and set BLENDER_SYSTEM_SCRIPTS patched_dir = Path("/home/user/app/blender_scripts_patched") if patched_dir.is_dir(): log.info("_patch_blender_bvh_addon: patched dir already exists at %s", patched_dir) os.environ["BLENDER_SYSTEM_SCRIPTS"] = str(patched_dir) return shutil.copytree(sys_scripts, patched_dir) patch_target = patched_dir / "addons/io_anim_bvh/import_bvh.py" text = patch_target.read_text() text = text.replace("'rU'", "'r'").replace('"rU"', '"r"') patch_target.write_text(text) os.environ["BLENDER_SYSTEM_SCRIPTS"] = str(patched_dir) log.info("_patch_blender_bvh_addon: created patched scripts at %s, set BLENDER_SYSTEM_SCRIPTS", patched_dir) def _ensure_blender_numpy() -> None: """Install numpy so Blender's glTF2 addon can export GLB. On HF Gradio Spaces (Debian Trixie), apt Blender links against python3.11 but the Space's default ``pip3`` targets python3.12+. Running ``pip3 install numpy`` installs for the wrong interpreter. Strategy: 1. Probe Blender for ``sys.version_info`` to know the Python version. 2. Try ``python -m pip install numpy`` (works on tarball Blender). 3. If that fails (apt Blender has no pip for 3.11), install numpy to a ``--target`` directory via the system pip, then set ``PYTHONPATH`` so Blender's Python can find it. 4. Verify by actually importing numpy inside Blender. Idempotent: probes numpy inside Blender first (using sys.exit to get a reliable return code — Blender returns 0 on --python-expr exceptions otherwise). """ import shutil as _shutil import subprocess as _subproc from pathlib import Path as _Path blender = os.environ.get("BLENDER_BIN", "").strip() or _shutil.which("blender") if not blender or not _Path(blender).exists(): log.warning("Blender not found — skipping numpy install for GLB export") return # --- Step 1: probe Blender for numpy + Python version --- # Use sys.exit(code) to get a reliable return code from Blender. probe_script = ( "import sys\n" "try:\n" " import numpy; print('NUMPY_OK', numpy.__version__); sys.exit(0)\n" "except ImportError:\n" " print('NUMPY_MISSING')\n" " print('PYVER', f'{sys.version_info.major}.{sys.version_info.minor}')\n" " sys.exit(42)\n" ) probe = _subproc.run( [blender, "--background", "--python-expr", probe_script], capture_output=True, text=True, timeout=30, ) # Parse stdout for our markers probe_data: dict[str, str] = {} for line in probe.stdout.splitlines(): parts = line.split(maxsplit=1) if len(parts) >= 1 and parts[0] in ("NUMPY_OK", "NUMPY_MISSING", "PYVER"): probe_data[parts[0]] = parts[1].strip() if len(parts) == 2 else "" if "NUMPY_OK" in probe_data: log.info("Blender numpy: already present (numpy %s, skip)", probe_data["NUMPY_OK"]) return py_ver = probe_data.get("PYVER", "") log.info("Blender Python version: %s, numpy missing — installing", py_ver) # --- Step 2: get pip working for Blender's Python, then install numpy --- blender_python = _shutil.which(f"python{py_ver}") if py_ver else None if not blender_python and py_ver: candidate = f"/usr/bin/python{py_ver}" if _Path(candidate).is_file(): blender_python = candidate if not blender_python: log.warning("Cannot find python%s — skipping numpy install", py_ver) return log.info("Blender's Python: %s", blender_python) installed = False # Strategy A: try ensurepip + pip install (works on tarball Blender) _subproc.run( [blender_python, "-m", "ensurepip", "--upgrade"], capture_output=True, text=True, timeout=120, ) direct = _subproc.run( [blender_python, "-m", "pip", "install", "--no-cache-dir", "--break-system-packages", "numpy"], capture_output=True, text=True, timeout=300, ) if direct.returncode == 0: log.info("Installed numpy via %s -m pip", blender_python) installed = True # Strategy B: bootstrap pip for this Python via get-pip.py, then install if not installed: log.info("%s has no pip — bootstrapping via get-pip.py", blender_python) import urllib.request get_pip = "/tmp/get-pip.py" try: urllib.request.urlretrieve( "https://bootstrap.pypa.io/get-pip.py", get_pip, ) except Exception as e: log.warning("Failed to download get-pip.py: %s", e) # Continue to Strategy C else: bootstrap_pip = _subproc.run( [blender_python, get_pip, "--break-system-packages"], capture_output=True, text=True, timeout=120, ) if bootstrap_pip.returncode == 0: install_np = _subproc.run( [blender_python, "-m", "pip", "install", "--no-cache-dir", "--break-system-packages", "numpy"], capture_output=True, text=True, timeout=300, ) if install_np.returncode == 0: log.info("Installed numpy via get-pip.py + %s -m pip", blender_python) installed = True else: log.info("pip install after get-pip failed (rc=%s): %s", install_np.returncode, (install_np.stderr or "")[-300:]) else: log.info("get-pip.py failed (rc=%s): %s", bootstrap_pip.returncode, (bootstrap_pip.stderr or "")[-300:]) # Strategy C: pip download wheels for the correct python version, install with --target if not installed: log.info("Trying pip download with --python-version %s ...", py_ver) pip3 = _shutil.which("pip3") or _shutil.which("pip") if pip3: dl_dir = _Path("/tmp/numpy_wheels") dl_dir.mkdir(parents=True, exist_ok=True) target_dir = _Path("/home/user/app/blender_numpy_packages") target_dir.mkdir(parents=True, exist_ok=True) dl = _subproc.run( [pip3, "download", "--no-cache-dir", "--python-version", py_ver, "--abi", f"cp{py_ver.replace('.', '')}", "--platform", "manylinux2014_x86_64", "--only-binary=:all:", "-d", str(dl_dir), "numpy"], capture_output=True, text=True, timeout=120, ) if dl.returncode == 0: wheels = list(dl_dir.glob("*.whl")) if wheels: inst = _subproc.run( [pip3, "install", "--no-deps", "--no-cache-dir", "--break-system-packages", "--target", str(target_dir)] + [str(w) for w in wheels], capture_output=True, text=True, timeout=120, ) if inst.returncode == 0: existing = os.environ.get("PYTHONPATH", "") os.environ["PYTHONPATH"] = ( f"{target_dir}:{existing}" if existing else str(target_dir) ) log.info("Installed numpy wheels to %s, PYTHONPATH=%s", target_dir, os.environ["PYTHONPATH"]) installed = True if not installed: log.warning("All numpy install strategies failed for python%s", py_ver) return # --- Step 4: verify inside Blender --- verify_script = ( "import sys\n" "try:\n" " import numpy; print('VERIFY_OK', numpy.__version__); sys.exit(0)\n" "except ImportError as e:\n" " print('VERIFY_FAIL', e); sys.exit(1)\n" ) confirm = _subproc.run( [blender, "--background", "--python-expr", verify_script], capture_output=True, text=True, timeout=30, ) for line in confirm.stdout.splitlines(): if line.startswith("VERIFY_OK"): log.info("Blender numpy install verified: %s", line) return if line.startswith("VERIFY_FAIL"): log.warning("Blender numpy verification failed: %s", line) return log.warning("Blender numpy verification inconclusive (rc=%s, stderr=%s)", confirm.returncode, (confirm.stderr or "")[-300:]) _CONTAINER_SPECS_LOGGED = False def _log_container_specs() -> None: """Log CPU/RAM/Blender specs once per process for ZeroGPU diagnostics.""" global _CONTAINER_SPECS_LOGGED if _CONTAINER_SPECS_LOGGED: return _CONTAINER_SPECS_LOGGED = True import multiprocessing import time specs: dict[str, str] = {} # CPU counts specs["mp_cpu_count"] = str(multiprocessing.cpu_count()) specs["os_cpu_count"] = str(os.cpu_count()) # RAM try: page = os.sysconf("SC_PAGE_SIZE") pages = os.sysconf("SC_PHYS_PAGES") specs["total_ram_gb"] = f"{page * pages / 1024**3:.2f}" except (ValueError, OSError): specs["total_ram_gb"] = "unknown" try: with open("/proc/meminfo") as f: for line in f: if line.startswith("MemAvailable:"): kb = int(line.split()[1]) specs["avail_ram_gb"] = f"{kb / 1024**2:.2f}" break else: specs["avail_ram_gb"] = "unknown" except OSError: specs["avail_ram_gb"] = "unknown" # CPU model try: with open("/proc/cpuinfo") as f: for line in f: if line.startswith("model name"): specs["cpu_model"] = line.split(":", 1)[1].strip() break else: specs["cpu_model"] = "unknown" except OSError: specs["cpu_model"] = "unknown" # Blender cold-start time import shutil as _shutil blender = os.environ.get("BLENDER_BIN", "").strip() or _shutil.which("blender") if blender and Path(blender).exists(): try: t0 = time.monotonic() subprocess.run( [blender, "--version"], capture_output=True, text=True, timeout=60, ) specs["blender_startup_s"] = f"{time.monotonic() - t0:.3f}" except Exception as e: specs["blender_startup_s"] = f"error:{e}" else: specs["blender_startup_s"] = "not_found" # cgroup limits (ZeroGPU containers use cgroup v2) try: with open("/sys/fs/cgroup/cpu.max") as f: specs["cgroup_cpu_max"] = f.read().strip() except Exception: specs["cgroup_cpu_max"] = "unavailable" try: with open("/sys/fs/cgroup/memory.max") as f: raw = f.read().strip() if raw == "max": specs["cgroup_mem_max"] = "max (unlimited)" else: specs["cgroup_mem_max"] = f"{int(raw) / 1024**3:.2f}GB" except Exception: specs["cgroup_mem_max"] = "unavailable" parts = " | ".join(f"{k}={v}" for k, v in specs.items()) log.info("[CONTAINER_SPECS] %s", parts) def bootstrap_external_repos() -> None: """Idempotent. Clone external repos + download checkpoints if not already present. No-op in Docker mode.""" if _docker_mode(): log.info("Docker mode detected (/opt/* paths present) — skip bootstrap") return log.info("Bootstrap mode (HF Gradio or fresh OSS-local) — base=%s", EXTERNAL_DIR) gh_token = os.environ.get("gh_token") or os.environ.get("GH_TOKEN") hf_token = os.environ.get("hf_token") or os.environ.get("HF_TOKEN") _install_blender_portable() _install_gltfpack() _clone_all(gh_token) _download_checkpoints(hf_token) _download_private_characters(hf_token) _set_env_for_external() _patch_mdm_model_util() _patch_priormdm_source() _patch_blender_bvh_addon() _ensure_blender_numpy() # Kimodo escape-hatch venv build runs in a background thread so the Gradio # UI comes up promptly with MDM/MoMask. ENABLE_KIMODO=false disables (kill # switch). Per [[No silent fallback in dev mode]]: any failure writes the # .kimodo_failed sentinel and is surfaced to the UI; we never silently # degrade. if os.environ.get("ENABLE_KIMODO", "true").strip().lower() != "false": _set_env_for_kimodo() _install_kimodo_async(hf_token) else: log.info("ENABLE_KIMODO=false — Kimodo bootstrap skipped") _log_container_specs() log.info("Bootstrap complete: external repos at %s", EXTERNAL_DIR) if __name__ == "__main__": logging.basicConfig( level="INFO", format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) bootstrap_external_repos()