Files
studio/studio_compat/__init__.py
T
hanzo-dev a9165125b5 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).
2026-07-04 13:19:09 -07:00

134 lines
4.7 KiB
Python

"""
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()