studio packs: vendor top ComfyUI node packs as first-class hanzo packages

Add a comfy->studio import shim so upstream ComfyUI custom nodes run
unmodified against this fork's renamed core, then vendor the popular packs
(Impact-Pack/Subpack, IPAdapter_plus, controlnet_aux, Ultimate SD Upscale,
Frame-Interpolation, VideoHelperSuite, rgthree, Custom-Scripts, KJNodes,
cg-use-everywhere, SAM2, segment_anything) pinned to immutable commits and
reproducibly rebuilt in the image.

- studio_compat: one meta-path finder aliasing comfy*->studio* and mirroring
  Comfy*->Studio* API classes (the deferred phase-4 of scripts/rename_modules.py,
  done with the 3.12 find_spec API); activated by one import at top of main.py.
- nodes.py: honor the upstream `comfy_entrypoint` V3 extension name too.
- custom_nodes/hanzo-packs.txt: pinned pack manifest (SHAs + SPDX licenses).
- custom_nodes-requirements.txt + scripts/install_custom_nodes.sh: pack deps
  with the cu130 torch / NumPy 2.x / Transformers 5.x stack locked (derived
  from the live env, never downgraded); ultralytics installed --no-deps.
- Dockerfile/.dockerignore/.gitignore: image vendors the packs at build.

Live GB10 worker verified: node types 644 -> 1290 (+646), 0 import failures.
was-node-suite quarantined (hard numba dep incompatible with NumPy 2.5).
This commit is contained in:
hanzo-dev
2026-07-04 13:19:09 -07:00
parent a9d77be4e7
commit a9165125b5
9 changed files with 318 additions and 7 deletions
+8 -2
View File
@@ -1,7 +1,11 @@
models/
output/
input/
custom_nodes/
custom_nodes/*
# ...but the pack manifest must reach the build context so the image can vendor
# the packs reproducibly (scripts/install_custom_nodes.sh clones them at build).
# Local clones stay excluded so the image builds them fresh from the manifest.
!custom_nodes/hanzo-packs.txt
user/
temp/
orgs/
@@ -19,7 +23,9 @@ venv/
.ruff_cache/
tests/
tests-unit/
scripts/
scripts/*
# ...except the pack installer, which the image build invokes.
!scripts/install_custom_nodes.sh
notebooks/
*.swp
.env
+7 -2
View File
@@ -5,8 +5,12 @@ __pycache__/
!/input/example.png
/models/
/temp/
/custom_nodes/
!custom_nodes/example_node.py.example
# Ignore vendored pack contents but keep the directory listable so the tracked
# files below can be re-included (a file under a fully-excluded dir cannot be).
/custom_nodes/*
!/custom_nodes/example_node.py.example
# Pinned pack manifest is tracked; the pack sources it clones are not (vendored at build).
!/custom_nodes/hanzo-packs.txt
extra_model_paths.yaml
/.vs
.vscode/
@@ -24,3 +28,4 @@ web_custom_versions/
openapi.yaml
filtered-openapi.yaml
uv.lock
/custom_nodes_quarantine/
+5
View File
@@ -25,6 +25,11 @@ RUN chmod +x /tmp/branding/apply-branding.sh && /tmp/branding/apply-branding.sh
# Copy application code
COPY . .
# Vendor the Hanzo Studio custom-node packs (segmentation, controlnet, ipadapter,
# upscaling, video, utils) at their pinned commits and install their deps with the
# torch/numpy/transformers stack locked. See custom_nodes/hanzo-packs.txt.
RUN chmod +x scripts/install_custom_nodes.sh && scripts/install_custom_nodes.sh
# Create directories for runtime volumes
RUN mkdir -p models output input custom_nodes user
+51
View File
@@ -0,0 +1,51 @@
# Hand-resolved Python deps for the vendored custom-node packs (see
# custom_nodes/hanzo-packs.txt). Install via scripts/install_custom_nodes.sh, which
# locks the studio cu130 torch stack / NumPy 2.x / Transformers 5.x (derived from
# the live env) so none of these can downgrade them. Only deps NOT already in
# requirements.txt are listed; the studio-critical packages are locked, not here.
# `ultralytics` is intentionally absent -- the installer adds it with --no-deps so
# it cannot pull the non-headless opencv-python that fights opencv-*-headless.
# --- Impact-Pack / segment_anything: detailer + SAM masks ---
segment-anything
timm
scikit-image
piexif
dill
matplotlib
# --- Impact-Subpack (ultralytics detectors): light deps only; ultralytics itself
# is installed --no-deps by the installer, then these fill in its runtime needs ---
py-cpuinfo
ultralytics-thop
pandas
# --- comfyui_controlnet_aux: pose/depth/edge preprocessors ---
opencv-contrib-python-headless
onnxruntime
addict
yapf
omegaconf
ftfy
yacs
fvcore
trimesh
albumentations
mediapipe
python-dateutil
importlib_metadata
scikit-learn
# --- ComfyUI-KJNodes ---
color-matcher
mss
# --- VideoHelperSuite / Frame-Interpolation: video I/O ---
imageio
imageio-ffmpeg
# --- misc utils used across packs ---
gitpython
joblib
pilgram
fairscale
+35
View File
@@ -0,0 +1,35 @@
# Hanzo Studio custom-node packs -- pinned, reproducible, first-class "hanzo packages".
#
# Format: <dir_name> <git_url> <commit_sha> <spdx_license> # friendly-ref
#
# Installed by scripts/install_custom_nodes.sh (invoked in the Docker image build
# and runnable locally) so the studio.hanzo.ai image ships the same packs the
# local GB10 worker runs. Upstream packs import comfy.* / subclass io.ComfyNode /
# expose comfy_entrypoint -- studio_compat aliases those to the studio.* names at
# boot and the loader honors comfy_entrypoint, so packs run UNMODIFIED (no fork,
# no source edits). Python deps: custom_nodes-requirements.txt (+ -constraints.txt).
#
# Pinned to immutable commit SHAs. To bump a pack: replace its SHA + re-run the
# installer (patch-bump; never blind-track HEAD). All licenses are OSI (GPL/AGPL/
# MIT/Apache) -- none are non-commercial.
ComfyUI-Impact-Pack https://github.com/ltdrdata/ComfyUI-Impact-Pack.git 429d0159ad429e64d2b3916e6e7be9c22d025c3c GPL-3.0 # 8.28
ComfyUI-Impact-Subpack https://github.com/ltdrdata/ComfyUI-Impact-Subpack.git 50c7b71a6a224734cc9b21963c6d1926816a97f1 AGPL-3.0 # 1.3.4 Ultralytics detectors
ComfyUI_IPAdapter_plus https://github.com/cubiq/ComfyUI_IPAdapter_plus.git a0f451a5113cf9becb0847b92884cb10cbdec0ef GPL-3.0 # HEAD 2025-04-14
comfyui_controlnet_aux https://github.com/Fannovel16/comfyui_controlnet_aux.git e8b689a513c3e6b63edc44066560ca5919c0576e Apache-2.0 # HEAD 2026-04-13
ComfyUI_UltimateSDUpscale https://github.com/ssitu/ComfyUI_UltimateSDUpscale.git a5547db9e1d07d3318bb21e9e9c474f4c1e9c8df GPL-3.0 # HEAD 2026-06-22
ComfyUI-Frame-Interpolation https://github.com/Fannovel16/ComfyUI-Frame-Interpolation.git 26545cc2dd95bc3d27f056016300673bdeee78f5 MIT # HEAD 2026-03-29
ComfyUI-VideoHelperSuite https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite.git 4ee72c065db22c9d96c2427954dc69e7b908444b GPL-3.0 # HEAD 2026-05-14
rgthree-comfy https://github.com/rgthree/rgthree-comfy.git 27b4f4cdcf3b127c29d5d8135ac1536ecbd4c383 MIT # HEAD 2026-06-20
ComfyUI-Custom-Scripts https://github.com/pythongosssss/ComfyUI-Custom-Scripts.git 609f3afaa74b2f88ef9ce8d939626065e3247469 MIT # HEAD 2026-02-12
ComfyUI-KJNodes https://github.com/kijai/ComfyUI-KJNodes.git e27a505b3ba6ce42687fe00500deda103d9d6071 GPL-3.0 # HEAD 2026-07-01
cg-use-everywhere https://github.com/chrisgoringe/cg-use-everywhere.git 632ed7bb51bb18ceb03ccaefe1f34be8bd416500 Apache-2.0 # HEAD 2026-06-22
ComfyUI-segment-anything-2 https://github.com/kijai/ComfyUI-segment-anything-2.git 0c35fff5f382803e2310103357b5e985f5437f32 Apache-2.0 # HEAD 2025-09-28 SAM2
comfyui_segment_anything https://github.com/storyicon/comfyui_segment_anything.git ab6395596399d5048639cdab7e44ec9fae857a93 Apache-2.0 # HEAD GroundingDINO+SAM
# --- QUARANTINED (not installed; kept here as a record) -------------------------
# was-node-suite-comfyui https://github.com/WASasquatch/was-node-suite-comfyui.git ea935d1044ae5a26efa54ebeb18fe9020af49a45 MIT
# Reason: hard `from numba import jit` at import; numba has no release supporting
# the studio venv's NumPy 2.5 ("Numba needs NumPy 2.4 or less"). Re-enabling would
# require downgrading NumPy (forbidden -- breaks the cu130 torch stack) or waiting
# for a numba release that supports NumPy 2.5. Revisit then. License (MIT) is fine.
+1
View File
@@ -1,3 +1,4 @@
import studio_compat # noqa: F401 aliases comfy.* -> studio.* for upstream custom nodes
import studio.options
studio.options.enable_args_parsing()
+8 -3
View File
@@ -2258,9 +2258,14 @@ async def load_custom_node(module_path: str, ignore=set(), module_parent="custom
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None:
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
return True
# V3 Extension Definition
elif hasattr(module, "studio_entrypoint"):
entrypoint = getattr(module, "studio_entrypoint")
# V3 Extension Definition. Honor the upstream ComfyUI `comfy_entrypoint`
# name too, so vendored packs register through the V3 extension API
# unmodified (their ComfyExtension/io.ComfyNode are aliased to the
# Studio* classes by studio_compat).
elif hasattr(module, "studio_entrypoint") or hasattr(module, "comfy_entrypoint"):
entrypoint = getattr(module, "studio_entrypoint", None)
if entrypoint is None:
entrypoint = getattr(module, "comfy_entrypoint")
if not callable(entrypoint):
logging.warning(f"studio_entrypoint in {module_path} is not callable, skipping.")
return False
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# install_custom_nodes.sh -- vendor the Hanzo Studio custom-node packs.
#
# Reads custom_nodes/hanzo-packs.txt, clones each pack at its pinned commit (git,
# idempotent), then installs their hand-resolved Python deps
# (custom_nodes-requirements.txt) with the studio torch/numpy/transformers stack
# LOCKED so nothing gets downgraded. The lock is DERIVED FROM THE LIVE ENV (not a
# hardcoded file), so it is correct in both contexts:
# - Docker image build (CPU torch): STUDIO_PY=python pip install
# - Local uv venv (GB10, cu130 torch):
# STUDIO_PY=.venv/bin/python \
# STUDIO_PIP="uv pip install --python .venv/bin/python" \
# scripts/install_custom_nodes.sh
#
# Upstream `comfy.*` imports are aliased to `studio.*` at boot by studio_compat,
# so packs run unmodified. Idempotent; safe to re-run to reconcile pins.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CN="$REPO/custom_nodes"
MANIFEST="$CN/hanzo-packs.txt"
REQS="$REPO/custom_nodes-requirements.txt"
PY="${STUDIO_PY:-python}"
PIP="${STUDIO_PIP:-python -m pip install}"
CGEN="$(mktemp)"
trap 'rm -f "$CGEN"' EXIT
log() { printf '[packs] %s\n' "$*"; }
# 0. Derive a constraints file locking the studio-critical packages to whatever
# versions are ALREADY installed, so pack deps can never move the torch stack,
# NumPy 2.x or Transformers 5.x. A dep that is incompatible surfaces as a
# resolver error instead of silently breaking the runtime.
"$PY" - > "$CGEN" <<'PY'
import importlib.metadata as md
CRIT = ["torch", "torchvision", "torchaudio", "torchsde", "numpy",
"transformers", "tokenizers", "pillow", "scipy", "timm",
"safetensors", "huggingface-hub"]
for name in CRIT:
try:
print(f"{name}=={md.version(name)}")
except md.PackageNotFoundError:
pass
PY
log "locked stack:"; sed 's/^/[packs] /' "$CGEN"
# 1. Clone / pin every pack from the manifest (comment + blank lines skipped).
log "reconciling packs from $MANIFEST"
while read -r name url sha _rest; do
case "$name" in ''|\#*) continue ;; esac
dir="$CN/$name"
if [ -d "$dir/.git" ]; then
git -C "$dir" fetch -q origin "$sha" 2>/dev/null || git -C "$dir" fetch -q --all --tags
else
git clone -q --filter=blob:none "$url" "$dir"
fi
git -C "$dir" checkout -q "$sha"
log "pinned $name @ $(git -C "$dir" rev-parse --short HEAD)"
done < "$MANIFEST"
# 2. Install pack dependencies with the derived stack lock.
log "installing pack dependencies"
$PIP -c "$CGEN" -r "$REQS"
# 3. ultralytics (AGPL detectors for Impact-Subpack): --no-deps so it cannot pull
# the non-headless opencv-python that would fight opencv-*-headless.
log "installing ultralytics (--no-deps)"
$PIP -c "$CGEN" --no-deps ultralytics
log "done -- $(grep -cvE '^\s*(#|$)' "$MANIFEST") packs pinned"
+133
View File
@@ -0,0 +1,133 @@
"""
studio_compat -- import-compat shim for upstream ComfyUI custom nodes.
Hanzo Studio renamed ComfyUI's internal packages (``comfy`` -> ``studio``,
``comfy_extras`` -> ``studio_extras``, ... and ``comfy/comfy_types`` ->
``studio/node_types``; see ``scripts/rename_modules.py``). Third-party custom
node packs import the upstream ``comfy*`` names, so they fail to import against
this fork.
This module installs ONE meta-path finder that lazily aliases every ``comfy*``
import to its ``studio*`` counterpart, so vendored packs run unmodified -- no
forking, no source edits, one place, one way. It is the compatibility redirect
that ``scripts/rename_modules.py`` deferred (its phase 4), implemented with the
modern ``find_spec`` API (``find_module``/``load_module`` were removed in
Python 3.12).
Activated once, from the top of ``main.py``::
import studio_compat # noqa: F401
"""
import importlib
import importlib.abc
import importlib.machinery
import sys
# Root package aliases: upstream ComfyUI name -> Hanzo Studio name.
_ROOTS = {
"comfy": "studio",
"comfy_extras": "studio_extras",
"comfy_api": "studio_api",
"comfy_api_nodes": "studio_api_nodes",
"comfy_config": "studio_config",
"comfy_execution": "studio_execution",
}
# Submodule renames applied to the target name after the root swap.
# ComfyUI's ``comfy/comfy_types`` lives at ``studio/node_types`` in this fork.
_SUBMODULE_RENAMES = {
"studio.comfy_types": "studio.node_types",
}
# External pip packages that share the ``comfy*``/``comfyui*`` prefix but are
# NOT part of the renamed core -- never alias these (they resolve normally).
# Mirrors the EXTERNAL_PACKAGES set in scripts/rename_modules.py.
_EXTERNAL = frozenset({
"comfy_kitchen",
"comfy_aimdo",
"comfyui_manager",
"comfyui_frontend_package",
"comfyui_workflow_templates",
"comfyui_embedded_docs",
})
def _target_name(fullname):
"""Return the ``studio*`` module name that ``fullname`` aliases, or None."""
root = fullname.split(".", 1)[0]
if root in _EXTERNAL or root not in _ROOTS:
return None
target = _ROOTS[root] + fullname[len(root):]
for src, dst in _SUBMODULE_RENAMES.items():
if target == src or target.startswith(src + "."):
target = dst + target[len(src):]
break
return target
# ---------------------------------------------------------------------------
# Class-name compatibility.
#
# The fork mechanically swapped the ``Comfy`` prefix for ``Studio`` on the
# public node-API classes (comfy_api.latest.ComfyExtension -> StudioExtension;
# comfy_api.latest.io.ComfyNode -> studio_api.latest.io.StudioNode, ...).
# Newer packs subclass ``io.ComfyNode`` / return ``ComfyExtension``. We apply
# the inverse swap as attribute aliases, lazily, the first time a pack imports
# the aliased module -- by which point studio_api.latest is safely importable.
# ---------------------------------------------------------------------------
def _mirror_comfy_prefix(module):
"""Bind ``Comfy<X>`` aliases for every public ``Studio<X>`` in ``module``."""
for name in list(vars(module)):
if name.startswith("Studio"):
alias = "Comfy" + name[len("Studio"):]
if not hasattr(module, alias):
setattr(module, alias, getattr(module, name))
def _mirror_studio_api_latest(module):
_mirror_comfy_prefix(module)
io_mod = getattr(module, "io", None) # studio_api.latest._io_public
if io_mod is not None:
_mirror_comfy_prefix(io_mod)
# target module name -> hook(module) run once, right after it is aliased.
_POST_IMPORT = {
"studio_api.latest": _mirror_studio_api_latest,
}
class _AliasLoader(importlib.abc.Loader):
def __init__(self, target):
self._target = target
def create_module(self, spec):
# Import (or reuse) the real module and hand it back as the alias.
# Returning an already-initialized module is why exec_module is a no-op;
# _init_module_attrs(override=False) preserves the target's __name__ etc.
module = importlib.import_module(self._target)
hook = _POST_IMPORT.get(self._target)
if hook is not None:
hook(module)
return module
def exec_module(self, module):
pass
class _AliasFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
name = _target_name(fullname)
if name is None:
return None
return importlib.machinery.ModuleSpec(fullname, _AliasLoader(name))
def install():
"""Insert the alias finder at the head of ``sys.meta_path`` (idempotent)."""
if not any(isinstance(f, _AliasFinder) for f in sys.meta_path):
sys.meta_path.insert(0, _AliasFinder())
install()