mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-02-17 00:43:48 +08:00
Merge branch 'master' into feat/api-nodes/bria-rmbg
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Build package / Build Test (3.11) (push) Waiting to run
Build package / Build Test (3.10) (push) Waiting to run
Build package / Build Test (3.12) (push) Waiting to run
Build package / Build Test (3.13) (push) Waiting to run
Build package / Build Test (3.14) (push) Waiting to run
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Build package / Build Test (3.11) (push) Waiting to run
Build package / Build Test (3.10) (push) Waiting to run
Build package / Build Test (3.12) (push) Waiting to run
Build package / Build Test (3.13) (push) Waiting to run
Build package / Build Test (3.14) (push) Waiting to run
This commit is contained in:
commit
a5c65ab84c
105
app/node_replace_manager.py
Normal file
105
app/node_replace_manager.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, TypedDict
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from comfy_api.latest._io_public import NodeReplace
|
||||||
|
|
||||||
|
from comfy_execution.graph_utils import is_link
|
||||||
|
import nodes
|
||||||
|
|
||||||
|
class NodeStruct(TypedDict):
|
||||||
|
inputs: dict[str, str | int | float | bool | tuple[str, int]]
|
||||||
|
class_type: str
|
||||||
|
_meta: dict[str, str]
|
||||||
|
|
||||||
|
def copy_node_struct(node_struct: NodeStruct, empty_inputs: bool = False) -> NodeStruct:
|
||||||
|
new_node_struct = node_struct.copy()
|
||||||
|
if empty_inputs:
|
||||||
|
new_node_struct["inputs"] = {}
|
||||||
|
else:
|
||||||
|
new_node_struct["inputs"] = node_struct["inputs"].copy()
|
||||||
|
new_node_struct["_meta"] = node_struct["_meta"].copy()
|
||||||
|
return new_node_struct
|
||||||
|
|
||||||
|
|
||||||
|
class NodeReplaceManager:
|
||||||
|
"""Manages node replacement registrations."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._replacements: dict[str, list[NodeReplace]] = {}
|
||||||
|
|
||||||
|
def register(self, node_replace: NodeReplace):
|
||||||
|
"""Register a node replacement mapping."""
|
||||||
|
self._replacements.setdefault(node_replace.old_node_id, []).append(node_replace)
|
||||||
|
|
||||||
|
def get_replacement(self, old_node_id: str) -> list[NodeReplace] | None:
|
||||||
|
"""Get replacements for an old node ID."""
|
||||||
|
return self._replacements.get(old_node_id)
|
||||||
|
|
||||||
|
def has_replacement(self, old_node_id: str) -> bool:
|
||||||
|
"""Check if a replacement exists for an old node ID."""
|
||||||
|
return old_node_id in self._replacements
|
||||||
|
|
||||||
|
def apply_replacements(self, prompt: dict[str, NodeStruct]):
|
||||||
|
connections: dict[str, list[tuple[str, str, int]]] = {}
|
||||||
|
need_replacement: set[str] = set()
|
||||||
|
for node_number, node_struct in prompt.items():
|
||||||
|
class_type = node_struct["class_type"]
|
||||||
|
# need replacement if not in NODE_CLASS_MAPPINGS and has replacement
|
||||||
|
if class_type not in nodes.NODE_CLASS_MAPPINGS.keys() and self.has_replacement(class_type):
|
||||||
|
need_replacement.add(node_number)
|
||||||
|
# keep track of connections
|
||||||
|
for input_id, input_value in node_struct["inputs"].items():
|
||||||
|
if is_link(input_value):
|
||||||
|
conn_number = input_value[0]
|
||||||
|
connections.setdefault(conn_number, []).append((node_number, input_id, input_value[1]))
|
||||||
|
for node_number in need_replacement:
|
||||||
|
node_struct = prompt[node_number]
|
||||||
|
class_type = node_struct["class_type"]
|
||||||
|
replacements = self.get_replacement(class_type)
|
||||||
|
if replacements is None:
|
||||||
|
continue
|
||||||
|
# just use the first replacement
|
||||||
|
replacement = replacements[0]
|
||||||
|
new_node_id = replacement.new_node_id
|
||||||
|
# if replacement is not a valid node, skip trying to replace it as will only cause confusion
|
||||||
|
if new_node_id not in nodes.NODE_CLASS_MAPPINGS.keys():
|
||||||
|
continue
|
||||||
|
# first, replace node id (class_type)
|
||||||
|
new_node_struct = copy_node_struct(node_struct, empty_inputs=True)
|
||||||
|
new_node_struct["class_type"] = new_node_id
|
||||||
|
# TODO: consider replacing display_name in _meta as well for error reporting purposes; would need to query node schema
|
||||||
|
# second, replace inputs
|
||||||
|
if replacement.input_mapping is not None:
|
||||||
|
for input_map in replacement.input_mapping:
|
||||||
|
if "set_value" in input_map:
|
||||||
|
new_node_struct["inputs"][input_map["new_id"]] = input_map["set_value"]
|
||||||
|
elif "old_id" in input_map:
|
||||||
|
new_node_struct["inputs"][input_map["new_id"]] = node_struct["inputs"][input_map["old_id"]]
|
||||||
|
# finalize input replacement
|
||||||
|
prompt[node_number] = new_node_struct
|
||||||
|
# third, replace outputs
|
||||||
|
if replacement.output_mapping is not None:
|
||||||
|
# re-mapping outputs requires changing the input values of nodes that receive connections from this one
|
||||||
|
if node_number in connections:
|
||||||
|
for conns in connections[node_number]:
|
||||||
|
conn_node_number, conn_input_id, old_output_idx = conns
|
||||||
|
for output_map in replacement.output_mapping:
|
||||||
|
if output_map["old_idx"] == old_output_idx:
|
||||||
|
new_output_idx = output_map["new_idx"]
|
||||||
|
previous_input = prompt[conn_node_number]["inputs"][conn_input_id]
|
||||||
|
previous_input[1] = new_output_idx
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""Serialize all replacements to dict."""
|
||||||
|
return {
|
||||||
|
k: [v.as_dict() for v in v_list]
|
||||||
|
for k, v_list in self._replacements.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def add_routes(self, routes):
|
||||||
|
@routes.get("/node_replacements")
|
||||||
|
async def get_node_replacements(request):
|
||||||
|
return web.json_response(self.as_dict())
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import pickle
|
|
||||||
|
|
||||||
load = pickle.load
|
|
||||||
|
|
||||||
class Empty:
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Unpickler(pickle.Unpickler):
|
|
||||||
def find_class(self, module, name):
|
|
||||||
#TODO: safe unpickle
|
|
||||||
if module.startswith("pytorch_lightning"):
|
|
||||||
return Empty
|
|
||||||
return super().find_class(module, name)
|
|
||||||
@ -20,7 +20,7 @@
|
|||||||
import torch
|
import torch
|
||||||
import math
|
import math
|
||||||
import struct
|
import struct
|
||||||
import comfy.checkpoint_pickle
|
import comfy.memory_management
|
||||||
import safetensors.torch
|
import safetensors.torch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@ -38,26 +38,26 @@ import warnings
|
|||||||
MMAP_TORCH_FILES = args.mmap_torch_files
|
MMAP_TORCH_FILES = args.mmap_torch_files
|
||||||
DISABLE_MMAP = args.disable_mmap
|
DISABLE_MMAP = args.disable_mmap
|
||||||
|
|
||||||
ALWAYS_SAFE_LOAD = False
|
|
||||||
if hasattr(torch.serialization, "add_safe_globals"): # TODO: this was added in pytorch 2.4, the unsafe path should be removed once earlier versions are deprecated
|
if True: # ckpt/pt file whitelist for safe loading of old sd files
|
||||||
class ModelCheckpoint:
|
class ModelCheckpoint:
|
||||||
pass
|
pass
|
||||||
ModelCheckpoint.__module__ = "pytorch_lightning.callbacks.model_checkpoint"
|
ModelCheckpoint.__module__ = "pytorch_lightning.callbacks.model_checkpoint"
|
||||||
|
|
||||||
def scalar(*args, **kwargs):
|
def scalar(*args, **kwargs):
|
||||||
from numpy.core.multiarray import scalar as sc
|
return None
|
||||||
return sc(*args, **kwargs)
|
|
||||||
scalar.__module__ = "numpy.core.multiarray"
|
scalar.__module__ = "numpy.core.multiarray"
|
||||||
|
|
||||||
from numpy import dtype
|
from numpy import dtype
|
||||||
from numpy.dtypes import Float64DType
|
from numpy.dtypes import Float64DType
|
||||||
from _codecs import encode
|
|
||||||
|
def encode(*args, **kwargs): # no longer necessary on newer torch
|
||||||
|
return None
|
||||||
|
encode.__module__ = "_codecs"
|
||||||
|
|
||||||
torch.serialization.add_safe_globals([ModelCheckpoint, scalar, dtype, Float64DType, encode])
|
torch.serialization.add_safe_globals([ModelCheckpoint, scalar, dtype, Float64DType, encode])
|
||||||
ALWAYS_SAFE_LOAD = True
|
|
||||||
logging.info("Checkpoint files will always be loaded safely.")
|
logging.info("Checkpoint files will always be loaded safely.")
|
||||||
else:
|
|
||||||
logging.warning("Warning, you are using an old pytorch version and some ckpt/pt files might be loaded unsafely. Upgrading to 2.4 or above is recommended as older versions of pytorch are no longer supported.")
|
|
||||||
|
|
||||||
# Current as of safetensors 0.7.0
|
# Current as of safetensors 0.7.0
|
||||||
_TYPES = {
|
_TYPES = {
|
||||||
@ -140,11 +140,8 @@ def load_torch_file(ckpt, safe_load=False, device=None, return_metadata=False):
|
|||||||
if MMAP_TORCH_FILES:
|
if MMAP_TORCH_FILES:
|
||||||
torch_args["mmap"] = True
|
torch_args["mmap"] = True
|
||||||
|
|
||||||
if safe_load or ALWAYS_SAFE_LOAD:
|
|
||||||
pl_sd = torch.load(ckpt, map_location=device, weights_only=True, **torch_args)
|
pl_sd = torch.load(ckpt, map_location=device, weights_only=True, **torch_args)
|
||||||
else:
|
|
||||||
logging.warning("WARNING: loading {} unsafely, upgrade your pytorch to 2.4 or newer to load this file safely.".format(ckpt))
|
|
||||||
pl_sd = torch.load(ckpt, map_location=device, pickle_module=comfy.checkpoint_pickle)
|
|
||||||
if "state_dict" in pl_sd:
|
if "state_dict" in pl_sd:
|
||||||
sd = pl_sd["state_dict"]
|
sd = pl_sd["state_dict"]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -14,6 +14,7 @@ SERVER_FEATURE_FLAGS: dict[str, Any] = {
|
|||||||
"supports_preview_metadata": True,
|
"supports_preview_metadata": True,
|
||||||
"max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes
|
"max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes
|
||||||
"extension": {"manager": {"supports_v4": True}},
|
"extension": {"manager": {"supports_v4": True}},
|
||||||
|
"node_replacements": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,17 @@ class ComfyAPI_latest(ComfyAPIBase):
|
|||||||
VERSION = "latest"
|
VERSION = "latest"
|
||||||
STABLE = False
|
STABLE = False
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.node_replacement = self.NodeReplacement()
|
||||||
|
self.execution = self.Execution()
|
||||||
|
|
||||||
|
class NodeReplacement(ProxiedSingleton):
|
||||||
|
async def register(self, node_replace: io.NodeReplace) -> None:
|
||||||
|
"""Register a node replacement mapping."""
|
||||||
|
from server import PromptServer
|
||||||
|
PromptServer.instance.node_replace_manager.register(node_replace)
|
||||||
|
|
||||||
class Execution(ProxiedSingleton):
|
class Execution(ProxiedSingleton):
|
||||||
async def set_progress(
|
async def set_progress(
|
||||||
self,
|
self,
|
||||||
@ -73,8 +84,6 @@ class ComfyAPI_latest(ComfyAPIBase):
|
|||||||
image=to_display,
|
image=to_display,
|
||||||
)
|
)
|
||||||
|
|
||||||
execution: Execution
|
|
||||||
|
|
||||||
class ComfyExtension(ABC):
|
class ComfyExtension(ABC):
|
||||||
async def on_load(self) -> None:
|
async def on_load(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -2030,6 +2030,68 @@ class _UIOutput(ABC):
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class InputMapOldId(TypedDict):
|
||||||
|
"""Map an old node input to a new node input by ID."""
|
||||||
|
new_id: str
|
||||||
|
old_id: str
|
||||||
|
|
||||||
|
class InputMapSetValue(TypedDict):
|
||||||
|
"""Set a specific value for a new node input."""
|
||||||
|
new_id: str
|
||||||
|
set_value: Any
|
||||||
|
|
||||||
|
InputMap = InputMapOldId | InputMapSetValue
|
||||||
|
"""
|
||||||
|
Input mapping for node replacement. Type is inferred by dictionary keys:
|
||||||
|
- {"new_id": str, "old_id": str} - maps old input to new input
|
||||||
|
- {"new_id": str, "set_value": Any} - sets a specific value for new input
|
||||||
|
"""
|
||||||
|
|
||||||
|
class OutputMap(TypedDict):
|
||||||
|
"""Map outputs of node replacement via indexes."""
|
||||||
|
new_idx: int
|
||||||
|
old_idx: int
|
||||||
|
|
||||||
|
class NodeReplace:
|
||||||
|
"""
|
||||||
|
Defines a possible node replacement, mapping inputs and outputs of the old node to the new node.
|
||||||
|
|
||||||
|
Also supports assigning specific values to the input widgets of the new node.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_node_id: The class name of the new replacement node.
|
||||||
|
old_node_id: The class name of the deprecated node.
|
||||||
|
old_widget_ids: Ordered list of input IDs for widgets that may not have an input slot
|
||||||
|
connected. The workflow JSON stores widget values by their relative position index,
|
||||||
|
not by ID. This list maps those positional indexes to input IDs, enabling the
|
||||||
|
replacement system to correctly identify widget values during node migration.
|
||||||
|
input_mapping: List of input mappings from old node to new node.
|
||||||
|
output_mapping: List of output mappings from old node to new node.
|
||||||
|
"""
|
||||||
|
def __init__(self,
|
||||||
|
new_node_id: str,
|
||||||
|
old_node_id: str,
|
||||||
|
old_widget_ids: list[str] | None=None,
|
||||||
|
input_mapping: list[InputMap] | None=None,
|
||||||
|
output_mapping: list[OutputMap] | None=None,
|
||||||
|
):
|
||||||
|
self.new_node_id = new_node_id
|
||||||
|
self.old_node_id = old_node_id
|
||||||
|
self.old_widget_ids = old_widget_ids
|
||||||
|
self.input_mapping = input_mapping
|
||||||
|
self.output_mapping = output_mapping
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""Create serializable representation of the node replacement."""
|
||||||
|
return {
|
||||||
|
"new_node_id": self.new_node_id,
|
||||||
|
"old_node_id": self.old_node_id,
|
||||||
|
"old_widget_ids": self.old_widget_ids,
|
||||||
|
"input_mapping": list(self.input_mapping) if self.input_mapping else None,
|
||||||
|
"output_mapping": list(self.output_mapping) if self.output_mapping else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"FolderType",
|
"FolderType",
|
||||||
"UploadType",
|
"UploadType",
|
||||||
@ -2121,4 +2183,5 @@ __all__ = [
|
|||||||
"ImageCompare",
|
"ImageCompare",
|
||||||
"PriceBadgeDepends",
|
"PriceBadgeDepends",
|
||||||
"PriceBadge",
|
"PriceBadge",
|
||||||
|
"NodeReplace",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -64,3 +64,23 @@ class To3DProTaskResultResponse(BaseModel):
|
|||||||
|
|
||||||
class To3DProTaskQueryRequest(BaseModel):
|
class To3DProTaskQueryRequest(BaseModel):
|
||||||
JobId: str = Field(...)
|
JobId: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class To3DUVFileInput(BaseModel):
|
||||||
|
Type: str = Field(..., description="File type: GLB, OBJ, or FBX")
|
||||||
|
Url: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class To3DUVTaskRequest(BaseModel):
|
||||||
|
File: To3DUVFileInput = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class TextureEditImageInfo(BaseModel):
|
||||||
|
Url: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class TextureEditTaskRequest(BaseModel):
|
||||||
|
File3D: To3DUVFileInput = Field(...)
|
||||||
|
Image: TextureEditImageInfo | None = Field(None)
|
||||||
|
Prompt: str | None = Field(None)
|
||||||
|
EnablePBR: bool | None = Field(None)
|
||||||
|
|||||||
@ -1,31 +1,48 @@
|
|||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from comfy_api.latest import IO, ComfyExtension, Input
|
from comfy_api.latest import IO, ComfyExtension, Input, Types
|
||||||
from comfy_api_nodes.apis.hunyuan3d import (
|
from comfy_api_nodes.apis.hunyuan3d import (
|
||||||
Hunyuan3DViewImage,
|
Hunyuan3DViewImage,
|
||||||
InputGenerateType,
|
InputGenerateType,
|
||||||
ResultFile3D,
|
ResultFile3D,
|
||||||
|
TextureEditTaskRequest,
|
||||||
To3DProTaskCreateResponse,
|
To3DProTaskCreateResponse,
|
||||||
To3DProTaskQueryRequest,
|
To3DProTaskQueryRequest,
|
||||||
To3DProTaskRequest,
|
To3DProTaskRequest,
|
||||||
To3DProTaskResultResponse,
|
To3DProTaskResultResponse,
|
||||||
|
To3DUVFileInput,
|
||||||
|
To3DUVTaskRequest,
|
||||||
)
|
)
|
||||||
from comfy_api_nodes.util import (
|
from comfy_api_nodes.util import (
|
||||||
ApiEndpoint,
|
ApiEndpoint,
|
||||||
download_url_to_file_3d,
|
download_url_to_file_3d,
|
||||||
|
download_url_to_image_tensor,
|
||||||
downscale_image_tensor_by_max_side,
|
downscale_image_tensor_by_max_side,
|
||||||
poll_op,
|
poll_op,
|
||||||
sync_op,
|
sync_op,
|
||||||
|
upload_3d_model_to_comfyapi,
|
||||||
upload_image_to_comfyapi,
|
upload_image_to_comfyapi,
|
||||||
validate_image_dimensions,
|
validate_image_dimensions,
|
||||||
validate_string,
|
validate_string,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_file_from_response(response_objs: list[ResultFile3D], file_type: str) -> ResultFile3D | None:
|
def _is_tencent_rate_limited(status: int, body: object) -> bool:
|
||||||
|
return (
|
||||||
|
status == 400
|
||||||
|
and isinstance(body, dict)
|
||||||
|
and "RequestLimitExceeded" in str(body.get("Response", {}).get("Error", {}).get("Code", ""))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_from_response(
|
||||||
|
response_objs: list[ResultFile3D], file_type: str, raise_if_not_found: bool = True
|
||||||
|
) -> ResultFile3D | None:
|
||||||
for i in response_objs:
|
for i in response_objs:
|
||||||
if i.Type.lower() == file_type.lower():
|
if i.Type.lower() == file_type.lower():
|
||||||
return i
|
return i
|
||||||
|
if raise_if_not_found:
|
||||||
|
raise ValueError(f"'{file_type}' file type is not found in the response.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +52,7 @@ class TencentTextToModelNode(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="TencentTextToModelNode",
|
node_id="TencentTextToModelNode",
|
||||||
display_name="Hunyuan3D: Text to Model (Pro)",
|
display_name="Hunyuan3D: Text to Model",
|
||||||
category="api node/3d/Tencent",
|
category="api node/3d/Tencent",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Combo.Input(
|
IO.Combo.Input(
|
||||||
@ -120,6 +137,7 @@ class TencentTextToModelNode(IO.ComfyNode):
|
|||||||
EnablePBR=generate_type.get("pbr", None),
|
EnablePBR=generate_type.get("pbr", None),
|
||||||
PolygonType=generate_type.get("polygon_type", None),
|
PolygonType=generate_type.get("polygon_type", None),
|
||||||
),
|
),
|
||||||
|
is_rate_limited=_is_tencent_rate_limited,
|
||||||
)
|
)
|
||||||
if response.Error:
|
if response.Error:
|
||||||
raise ValueError(f"Task creation failed with code {response.Error.Code}: {response.Error.Message}")
|
raise ValueError(f"Task creation failed with code {response.Error.Code}: {response.Error.Message}")
|
||||||
@ -131,11 +149,14 @@ class TencentTextToModelNode(IO.ComfyNode):
|
|||||||
response_model=To3DProTaskResultResponse,
|
response_model=To3DProTaskResultResponse,
|
||||||
status_extractor=lambda r: r.Status,
|
status_extractor=lambda r: r.Status,
|
||||||
)
|
)
|
||||||
glb_result = get_file_from_response(result.ResultFile3Ds, "glb")
|
|
||||||
obj_result = get_file_from_response(result.ResultFile3Ds, "obj")
|
|
||||||
file_glb = await download_url_to_file_3d(glb_result.Url, "glb", task_id=task_id) if glb_result else None
|
|
||||||
return IO.NodeOutput(
|
return IO.NodeOutput(
|
||||||
file_glb, file_glb, await download_url_to_file_3d(obj_result.Url, "obj", task_id=task_id) if obj_result else None
|
f"{task_id}.glb",
|
||||||
|
await download_url_to_file_3d(
|
||||||
|
get_file_from_response(result.ResultFile3Ds, "glb").Url, "glb", task_id=task_id
|
||||||
|
),
|
||||||
|
await download_url_to_file_3d(
|
||||||
|
get_file_from_response(result.ResultFile3Ds, "obj").Url, "obj", task_id=task_id
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -145,7 +166,7 @@ class TencentImageToModelNode(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="TencentImageToModelNode",
|
node_id="TencentImageToModelNode",
|
||||||
display_name="Hunyuan3D: Image(s) to Model (Pro)",
|
display_name="Hunyuan3D: Image(s) to Model",
|
||||||
category="api node/3d/Tencent",
|
category="api node/3d/Tencent",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Combo.Input(
|
IO.Combo.Input(
|
||||||
@ -268,6 +289,7 @@ class TencentImageToModelNode(IO.ComfyNode):
|
|||||||
EnablePBR=generate_type.get("pbr", None),
|
EnablePBR=generate_type.get("pbr", None),
|
||||||
PolygonType=generate_type.get("polygon_type", None),
|
PolygonType=generate_type.get("polygon_type", None),
|
||||||
),
|
),
|
||||||
|
is_rate_limited=_is_tencent_rate_limited,
|
||||||
)
|
)
|
||||||
if response.Error:
|
if response.Error:
|
||||||
raise ValueError(f"Task creation failed with code {response.Error.Code}: {response.Error.Message}")
|
raise ValueError(f"Task creation failed with code {response.Error.Code}: {response.Error.Message}")
|
||||||
@ -279,11 +301,257 @@ class TencentImageToModelNode(IO.ComfyNode):
|
|||||||
response_model=To3DProTaskResultResponse,
|
response_model=To3DProTaskResultResponse,
|
||||||
status_extractor=lambda r: r.Status,
|
status_extractor=lambda r: r.Status,
|
||||||
)
|
)
|
||||||
glb_result = get_file_from_response(result.ResultFile3Ds, "glb")
|
|
||||||
obj_result = get_file_from_response(result.ResultFile3Ds, "obj")
|
|
||||||
file_glb = await download_url_to_file_3d(glb_result.Url, "glb", task_id=task_id) if glb_result else None
|
|
||||||
return IO.NodeOutput(
|
return IO.NodeOutput(
|
||||||
file_glb, file_glb, await download_url_to_file_3d(obj_result.Url, "obj", task_id=task_id) if obj_result else None
|
f"{task_id}.glb",
|
||||||
|
await download_url_to_file_3d(
|
||||||
|
get_file_from_response(result.ResultFile3Ds, "glb").Url, "glb", task_id=task_id
|
||||||
|
),
|
||||||
|
await download_url_to_file_3d(
|
||||||
|
get_file_from_response(result.ResultFile3Ds, "obj").Url, "obj", task_id=task_id
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TencentModelTo3DUVNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="TencentModelTo3DUVNode",
|
||||||
|
display_name="Hunyuan3D: Model to UV",
|
||||||
|
category="api node/3d/Tencent",
|
||||||
|
description="Perform UV unfolding on a 3D model to generate UV texture. "
|
||||||
|
"Input model must have less than 30000 faces.",
|
||||||
|
inputs=[
|
||||||
|
IO.MultiType.Input(
|
||||||
|
"model_3d",
|
||||||
|
types=[IO.File3DGLB, IO.File3DOBJ, IO.File3DFBX, IO.File3DAny],
|
||||||
|
tooltip="Input 3D model (GLB, OBJ, or FBX)",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=1,
|
||||||
|
min=0,
|
||||||
|
max=2147483647,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed controls whether the node should re-run; "
|
||||||
|
"results are non-deterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.File3DOBJ.Output(display_name="OBJ"),
|
||||||
|
IO.File3DFBX.Output(display_name="FBX"),
|
||||||
|
IO.Image.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(expr='{"type":"usd","usd":0.2}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
SUPPORTED_FORMATS = {"glb", "obj", "fbx"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model_3d: Types.File3D,
|
||||||
|
seed: int,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
_ = seed
|
||||||
|
file_format = model_3d.format.lower()
|
||||||
|
if file_format not in cls.SUPPORTED_FORMATS:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported file format: '{file_format}'. "
|
||||||
|
f"Supported formats: {', '.join(sorted(cls.SUPPORTED_FORMATS))}."
|
||||||
|
)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-uv", method="POST"),
|
||||||
|
response_model=To3DProTaskCreateResponse,
|
||||||
|
data=To3DUVTaskRequest(
|
||||||
|
File=To3DUVFileInput(
|
||||||
|
Type=file_format.upper(),
|
||||||
|
Url=await upload_3d_model_to_comfyapi(cls, model_3d, file_format),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
is_rate_limited=_is_tencent_rate_limited,
|
||||||
|
)
|
||||||
|
if response.Error:
|
||||||
|
raise ValueError(f"Task creation failed with code {response.Error.Code}: {response.Error.Message}")
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-uv/query", method="POST"),
|
||||||
|
data=To3DProTaskQueryRequest(JobId=response.JobId),
|
||||||
|
response_model=To3DProTaskResultResponse,
|
||||||
|
status_extractor=lambda r: r.Status,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(
|
||||||
|
await download_url_to_file_3d(get_file_from_response(result.ResultFile3Ds, "obj").Url, "obj"),
|
||||||
|
await download_url_to_file_3d(get_file_from_response(result.ResultFile3Ds, "fbx").Url, "fbx"),
|
||||||
|
await download_url_to_image_tensor(get_file_from_response(result.ResultFile3Ds, "image").Url),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Tencent3DTextureEditNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="Tencent3DTextureEditNode",
|
||||||
|
display_name="Hunyuan3D: 3D Texture Edit",
|
||||||
|
category="api node/3d/Tencent",
|
||||||
|
description="After inputting the 3D model, perform 3D model texture redrawing.",
|
||||||
|
inputs=[
|
||||||
|
IO.MultiType.Input(
|
||||||
|
"model_3d",
|
||||||
|
types=[IO.File3DFBX, IO.File3DAny],
|
||||||
|
tooltip="3D model in FBX format. Model should have less than 100000 faces.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="Describes texture editing. Supports up to 1024 UTF-8 characters.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=2147483647,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed controls whether the node should re-run; "
|
||||||
|
"results are non-deterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.File3DGLB.Output(display_name="GLB"),
|
||||||
|
IO.File3DFBX.Output(display_name="FBX"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd": 0.6}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model_3d: Types.File3D,
|
||||||
|
prompt: str,
|
||||||
|
seed: int,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
_ = seed
|
||||||
|
file_format = model_3d.format.lower()
|
||||||
|
if file_format != "fbx":
|
||||||
|
raise ValueError(f"Unsupported file format: '{file_format}'. Only FBX format is supported.")
|
||||||
|
validate_string(prompt, field_name="prompt", min_length=1, max_length=1024)
|
||||||
|
model_url = await upload_3d_model_to_comfyapi(cls, model_3d, file_format)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-texture-edit", method="POST"),
|
||||||
|
response_model=To3DProTaskCreateResponse,
|
||||||
|
data=TextureEditTaskRequest(
|
||||||
|
File3D=To3DUVFileInput(Type=file_format.upper(), Url=model_url),
|
||||||
|
Prompt=prompt,
|
||||||
|
EnablePBR=True,
|
||||||
|
),
|
||||||
|
is_rate_limited=_is_tencent_rate_limited,
|
||||||
|
)
|
||||||
|
if response.Error:
|
||||||
|
raise ValueError(f"Task creation failed with code {response.Error.Code}: {response.Error.Message}")
|
||||||
|
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-texture-edit/query", method="POST"),
|
||||||
|
data=To3DProTaskQueryRequest(JobId=response.JobId),
|
||||||
|
response_model=To3DProTaskResultResponse,
|
||||||
|
status_extractor=lambda r: r.Status,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(
|
||||||
|
await download_url_to_file_3d(get_file_from_response(result.ResultFile3Ds, "glb").Url, "glb"),
|
||||||
|
await download_url_to_file_3d(get_file_from_response(result.ResultFile3Ds, "fbx").Url, "fbx"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Tencent3DPartNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="Tencent3DPartNode",
|
||||||
|
display_name="Hunyuan3D: 3D Part",
|
||||||
|
category="api node/3d/Tencent",
|
||||||
|
description="Automatically perform component identification and generation based on the model structure.",
|
||||||
|
inputs=[
|
||||||
|
IO.MultiType.Input(
|
||||||
|
"model_3d",
|
||||||
|
types=[IO.File3DFBX, IO.File3DAny],
|
||||||
|
tooltip="3D model in FBX format. Model should have less than 30000 faces.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=2147483647,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed controls whether the node should re-run; "
|
||||||
|
"results are non-deterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.File3DFBX.Output(display_name="FBX"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(expr='{"type":"usd","usd":0.6}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model_3d: Types.File3D,
|
||||||
|
seed: int,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
_ = seed
|
||||||
|
file_format = model_3d.format.lower()
|
||||||
|
if file_format != "fbx":
|
||||||
|
raise ValueError(f"Unsupported file format: '{file_format}'. Only FBX format is supported.")
|
||||||
|
model_url = await upload_3d_model_to_comfyapi(cls, model_3d, file_format)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-part", method="POST"),
|
||||||
|
response_model=To3DProTaskCreateResponse,
|
||||||
|
data=To3DUVTaskRequest(
|
||||||
|
File=To3DUVFileInput(Type=file_format.upper(), Url=model_url),
|
||||||
|
),
|
||||||
|
is_rate_limited=_is_tencent_rate_limited,
|
||||||
|
)
|
||||||
|
if response.Error:
|
||||||
|
raise ValueError(f"Task creation failed with code {response.Error.Code}: {response.Error.Message}")
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/tencent/hunyuan/3d-part/query", method="POST"),
|
||||||
|
data=To3DProTaskQueryRequest(JobId=response.JobId),
|
||||||
|
response_model=To3DProTaskResultResponse,
|
||||||
|
status_extractor=lambda r: r.Status,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(
|
||||||
|
await download_url_to_file_3d(get_file_from_response(result.ResultFile3Ds, "fbx").Url, "fbx"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -293,6 +561,9 @@ class TencentHunyuan3DExtension(ComfyExtension):
|
|||||||
return [
|
return [
|
||||||
TencentTextToModelNode,
|
TencentTextToModelNode,
|
||||||
TencentImageToModelNode,
|
TencentImageToModelNode,
|
||||||
|
# TencentModelTo3DUVNode,
|
||||||
|
# Tencent3DTextureEditNode,
|
||||||
|
Tencent3DPartNode,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,6 @@ class SupportedOpenAIModel(str, Enum):
|
|||||||
o1 = "o1"
|
o1 = "o1"
|
||||||
o3 = "o3"
|
o3 = "o3"
|
||||||
o1_pro = "o1-pro"
|
o1_pro = "o1-pro"
|
||||||
gpt_4o = "gpt-4o"
|
|
||||||
gpt_4_1 = "gpt-4.1"
|
gpt_4_1 = "gpt-4.1"
|
||||||
gpt_4_1_mini = "gpt-4.1-mini"
|
gpt_4_1_mini = "gpt-4.1-mini"
|
||||||
gpt_4_1_nano = "gpt-4.1-nano"
|
gpt_4_1_nano = "gpt-4.1-nano"
|
||||||
@ -649,11 +648,6 @@ class OpenAIChatNode(IO.ComfyNode):
|
|||||||
"usd": [0.01, 0.04],
|
"usd": [0.01, 0.04],
|
||||||
"format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
|
"format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
|
||||||
}
|
}
|
||||||
: $contains($m, "gpt-4o") ? {
|
|
||||||
"type": "list_usd",
|
|
||||||
"usd": [0.0025, 0.01],
|
|
||||||
"format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
|
|
||||||
}
|
|
||||||
: $contains($m, "gpt-4.1-nano") ? {
|
: $contains($m, "gpt-4.1-nano") ? {
|
||||||
"type": "list_usd",
|
"type": "list_usd",
|
||||||
"usd": [0.0001, 0.0004],
|
"usd": [0.0001, 0.0004],
|
||||||
|
|||||||
@ -33,6 +33,7 @@ from .download_helpers import (
|
|||||||
download_url_to_video_output,
|
download_url_to_video_output,
|
||||||
)
|
)
|
||||||
from .upload_helpers import (
|
from .upload_helpers import (
|
||||||
|
upload_3d_model_to_comfyapi,
|
||||||
upload_audio_to_comfyapi,
|
upload_audio_to_comfyapi,
|
||||||
upload_file_to_comfyapi,
|
upload_file_to_comfyapi,
|
||||||
upload_image_to_comfyapi,
|
upload_image_to_comfyapi,
|
||||||
@ -62,6 +63,7 @@ __all__ = [
|
|||||||
"sync_op",
|
"sync_op",
|
||||||
"sync_op_raw",
|
"sync_op_raw",
|
||||||
# Upload helpers
|
# Upload helpers
|
||||||
|
"upload_3d_model_to_comfyapi",
|
||||||
"upload_audio_to_comfyapi",
|
"upload_audio_to_comfyapi",
|
||||||
"upload_file_to_comfyapi",
|
"upload_file_to_comfyapi",
|
||||||
"upload_image_to_comfyapi",
|
"upload_image_to_comfyapi",
|
||||||
|
|||||||
@ -164,6 +164,27 @@ async def upload_video_to_comfyapi(
|
|||||||
return await upload_file_to_comfyapi(cls, video_bytes_io, filename, upload_mime_type, wait_label)
|
return await upload_file_to_comfyapi(cls, video_bytes_io, filename, upload_mime_type, wait_label)
|
||||||
|
|
||||||
|
|
||||||
|
_3D_MIME_TYPES = {
|
||||||
|
"glb": "model/gltf-binary",
|
||||||
|
"obj": "model/obj",
|
||||||
|
"fbx": "application/octet-stream",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def upload_3d_model_to_comfyapi(
|
||||||
|
cls: type[IO.ComfyNode],
|
||||||
|
model_3d: Types.File3D,
|
||||||
|
file_format: str,
|
||||||
|
) -> str:
|
||||||
|
"""Uploads a 3D model file to ComfyUI API and returns its download URL."""
|
||||||
|
return await upload_file_to_comfyapi(
|
||||||
|
cls,
|
||||||
|
model_3d.get_data(),
|
||||||
|
f"{uuid.uuid4()}.{file_format}",
|
||||||
|
_3D_MIME_TYPES.get(file_format, "application/octet-stream"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def upload_file_to_comfyapi(
|
async def upload_file_to_comfyapi(
|
||||||
cls: type[IO.ComfyNode],
|
cls: type[IO.ComfyNode],
|
||||||
file_bytes_io: BytesIO,
|
file_bytes_io: BytesIO,
|
||||||
|
|||||||
@ -655,6 +655,7 @@ class BatchImagesMasksLatentsNode(io.ComfyNode):
|
|||||||
batched = batch_masks(values)
|
batched = batch_masks(values)
|
||||||
return io.NodeOutput(batched)
|
return io.NodeOutput(batched)
|
||||||
|
|
||||||
|
|
||||||
class PostProcessingExtension(ComfyExtension):
|
class PostProcessingExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
|||||||
103
comfy_extras/nodes_replacements.py
Normal file
103
comfy_extras/nodes_replacements.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
from comfy_api.latest import ComfyExtension, io, ComfyAPI
|
||||||
|
|
||||||
|
api = ComfyAPI()
|
||||||
|
|
||||||
|
|
||||||
|
async def register_replacements():
|
||||||
|
"""Register all built-in node replacements."""
|
||||||
|
await register_replacements_longeredge()
|
||||||
|
await register_replacements_batchimages()
|
||||||
|
await register_replacements_upscaleimage()
|
||||||
|
await register_replacements_controlnet()
|
||||||
|
await register_replacements_load3d()
|
||||||
|
await register_replacements_preview3d()
|
||||||
|
await register_replacements_svdimg2vid()
|
||||||
|
await register_replacements_conditioningavg()
|
||||||
|
|
||||||
|
async def register_replacements_longeredge():
|
||||||
|
# No dynamic inputs here
|
||||||
|
await api.node_replacement.register(io.NodeReplace(
|
||||||
|
new_node_id="ImageScaleToMaxDimension",
|
||||||
|
old_node_id="ResizeImagesByLongerEdge",
|
||||||
|
old_widget_ids=["longer_edge"],
|
||||||
|
input_mapping=[
|
||||||
|
{"new_id": "image", "old_id": "images"},
|
||||||
|
{"new_id": "largest_size", "old_id": "longer_edge"},
|
||||||
|
{"new_id": "upscale_method", "set_value": "lanczos"},
|
||||||
|
],
|
||||||
|
# just to test the frontend output_mapping code, does nothing really here
|
||||||
|
output_mapping=[{"new_idx": 0, "old_idx": 0}],
|
||||||
|
))
|
||||||
|
|
||||||
|
async def register_replacements_batchimages():
|
||||||
|
# BatchImages node uses Autogrow
|
||||||
|
await api.node_replacement.register(io.NodeReplace(
|
||||||
|
new_node_id="BatchImagesNode",
|
||||||
|
old_node_id="ImageBatch",
|
||||||
|
input_mapping=[
|
||||||
|
{"new_id": "images.image0", "old_id": "image1"},
|
||||||
|
{"new_id": "images.image1", "old_id": "image2"},
|
||||||
|
],
|
||||||
|
))
|
||||||
|
|
||||||
|
async def register_replacements_upscaleimage():
|
||||||
|
# ResizeImageMaskNode uses DynamicCombo
|
||||||
|
await api.node_replacement.register(io.NodeReplace(
|
||||||
|
new_node_id="ResizeImageMaskNode",
|
||||||
|
old_node_id="ImageScaleBy",
|
||||||
|
old_widget_ids=["upscale_method", "scale_by"],
|
||||||
|
input_mapping=[
|
||||||
|
{"new_id": "input", "old_id": "image"},
|
||||||
|
{"new_id": "resize_type", "set_value": "scale by multiplier"},
|
||||||
|
{"new_id": "resize_type.multiplier", "old_id": "scale_by"},
|
||||||
|
{"new_id": "scale_method", "old_id": "upscale_method"},
|
||||||
|
],
|
||||||
|
))
|
||||||
|
|
||||||
|
async def register_replacements_controlnet():
|
||||||
|
# T2IAdapterLoader → ControlNetLoader
|
||||||
|
await api.node_replacement.register(io.NodeReplace(
|
||||||
|
new_node_id="ControlNetLoader",
|
||||||
|
old_node_id="T2IAdapterLoader",
|
||||||
|
input_mapping=[
|
||||||
|
{"new_id": "control_net_name", "old_id": "t2i_adapter_name"},
|
||||||
|
],
|
||||||
|
))
|
||||||
|
|
||||||
|
async def register_replacements_load3d():
|
||||||
|
# Load3DAnimation merged into Load3D
|
||||||
|
await api.node_replacement.register(io.NodeReplace(
|
||||||
|
new_node_id="Load3D",
|
||||||
|
old_node_id="Load3DAnimation",
|
||||||
|
))
|
||||||
|
|
||||||
|
async def register_replacements_preview3d():
|
||||||
|
# Preview3DAnimation merged into Preview3D
|
||||||
|
await api.node_replacement.register(io.NodeReplace(
|
||||||
|
new_node_id="Preview3D",
|
||||||
|
old_node_id="Preview3DAnimation",
|
||||||
|
))
|
||||||
|
|
||||||
|
async def register_replacements_svdimg2vid():
|
||||||
|
# Typo fix: SDV → SVD
|
||||||
|
await api.node_replacement.register(io.NodeReplace(
|
||||||
|
new_node_id="SVD_img2vid_Conditioning",
|
||||||
|
old_node_id="SDV_img2vid_Conditioning",
|
||||||
|
))
|
||||||
|
|
||||||
|
async def register_replacements_conditioningavg():
|
||||||
|
# Typo fix: trailing space in node name
|
||||||
|
await api.node_replacement.register(io.NodeReplace(
|
||||||
|
new_node_id="ConditioningAverage",
|
||||||
|
old_node_id="ConditioningAverage ",
|
||||||
|
))
|
||||||
|
|
||||||
|
class NodeReplacementsExtension(ComfyExtension):
|
||||||
|
async def on_load(self) -> None:
|
||||||
|
await register_replacements()
|
||||||
|
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> NodeReplacementsExtension:
|
||||||
|
return NodeReplacementsExtension()
|
||||||
2
nodes.py
2
nodes.py
@ -2264,6 +2264,7 @@ async def load_custom_node(module_path: str, ignore=set(), module_parent="custom
|
|||||||
if not isinstance(extension, ComfyExtension):
|
if not isinstance(extension, ComfyExtension):
|
||||||
logging.warning(f"comfy_entrypoint in {module_path} did not return a ComfyExtension, skipping.")
|
logging.warning(f"comfy_entrypoint in {module_path} did not return a ComfyExtension, skipping.")
|
||||||
return False
|
return False
|
||||||
|
await extension.on_load()
|
||||||
node_list = await extension.get_node_list()
|
node_list = await extension.get_node_list()
|
||||||
if not isinstance(node_list, list):
|
if not isinstance(node_list, list):
|
||||||
logging.warning(f"comfy_entrypoint in {module_path} did not return a list of nodes, skipping.")
|
logging.warning(f"comfy_entrypoint in {module_path} did not return a list of nodes, skipping.")
|
||||||
@ -2435,6 +2436,7 @@ async def init_builtin_extra_nodes():
|
|||||||
"nodes_lora_debug.py",
|
"nodes_lora_debug.py",
|
||||||
"nodes_color.py",
|
"nodes_color.py",
|
||||||
"nodes_toolkit.py",
|
"nodes_toolkit.py",
|
||||||
|
"nodes_replacements.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
import_failed = []
|
import_failed = []
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
comfyui-frontend-package==1.38.13
|
comfyui-frontend-package==1.38.14
|
||||||
comfyui-workflow-templates==0.8.38
|
comfyui-workflow-templates==0.8.38
|
||||||
comfyui-embedded-docs==0.4.1
|
comfyui-embedded-docs==0.4.1
|
||||||
torch
|
torch
|
||||||
|
|||||||
@ -40,6 +40,7 @@ from app.user_manager import UserManager
|
|||||||
from app.model_manager import ModelFileManager
|
from app.model_manager import ModelFileManager
|
||||||
from app.custom_node_manager import CustomNodeManager
|
from app.custom_node_manager import CustomNodeManager
|
||||||
from app.subgraph_manager import SubgraphManager
|
from app.subgraph_manager import SubgraphManager
|
||||||
|
from app.node_replace_manager import NodeReplaceManager
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from api_server.routes.internal.internal_routes import InternalRoutes
|
from api_server.routes.internal.internal_routes import InternalRoutes
|
||||||
from protocol import BinaryEventTypes
|
from protocol import BinaryEventTypes
|
||||||
@ -204,6 +205,7 @@ class PromptServer():
|
|||||||
self.model_file_manager = ModelFileManager()
|
self.model_file_manager = ModelFileManager()
|
||||||
self.custom_node_manager = CustomNodeManager()
|
self.custom_node_manager = CustomNodeManager()
|
||||||
self.subgraph_manager = SubgraphManager()
|
self.subgraph_manager = SubgraphManager()
|
||||||
|
self.node_replace_manager = NodeReplaceManager()
|
||||||
self.internal_routes = InternalRoutes(self)
|
self.internal_routes = InternalRoutes(self)
|
||||||
self.supports = ["custom_nodes_from_web"]
|
self.supports = ["custom_nodes_from_web"]
|
||||||
self.prompt_queue = execution.PromptQueue(self)
|
self.prompt_queue = execution.PromptQueue(self)
|
||||||
@ -887,6 +889,8 @@ class PromptServer():
|
|||||||
if "partial_execution_targets" in json_data:
|
if "partial_execution_targets" in json_data:
|
||||||
partial_execution_targets = json_data["partial_execution_targets"]
|
partial_execution_targets = json_data["partial_execution_targets"]
|
||||||
|
|
||||||
|
self.node_replace_manager.apply_replacements(prompt)
|
||||||
|
|
||||||
valid = await execution.validate_prompt(prompt_id, prompt, partial_execution_targets)
|
valid = await execution.validate_prompt(prompt_id, prompt, partial_execution_targets)
|
||||||
extra_data = {}
|
extra_data = {}
|
||||||
if "extra_data" in json_data:
|
if "extra_data" in json_data:
|
||||||
@ -995,6 +999,7 @@ class PromptServer():
|
|||||||
self.model_file_manager.add_routes(self.routes)
|
self.model_file_manager.add_routes(self.routes)
|
||||||
self.custom_node_manager.add_routes(self.routes, self.app, nodes.LOADED_MODULE_DIRS.items())
|
self.custom_node_manager.add_routes(self.routes, self.app, nodes.LOADED_MODULE_DIRS.items())
|
||||||
self.subgraph_manager.add_routes(self.routes, nodes.LOADED_MODULE_DIRS.items())
|
self.subgraph_manager.add_routes(self.routes, nodes.LOADED_MODULE_DIRS.items())
|
||||||
|
self.node_replace_manager.add_routes(self.routes)
|
||||||
self.app.add_subapp('/internal', self.internal_routes.get_app())
|
self.app.add_subapp('/internal', self.internal_routes.get_app())
|
||||||
|
|
||||||
# Prefix every route with /api for easier matching for delegation.
|
# Prefix every route with /api for easier matching for delegation.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user