mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-25 05:40:15 +08:00
Merge branch 'comfyanonymous:master' into master
This commit is contained in:
commit
dda057a4f4
2
.github/workflows/release-stable-all.yml
vendored
2
.github/workflows/release-stable-all.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
contents: "write"
|
contents: "write"
|
||||||
packages: "write"
|
packages: "write"
|
||||||
pull-requests: "read"
|
pull-requests: "read"
|
||||||
name: "Release NVIDIA Default (cu129)"
|
name: "Release NVIDIA Default (cu130)"
|
||||||
uses: ./.github/workflows/stable-release.yml
|
uses: ./.github/workflows/stable-release.yml
|
||||||
with:
|
with:
|
||||||
git_tag: ${{ inputs.git_tag }}
|
git_tag: ${{ inputs.git_tag }}
|
||||||
|
|||||||
@ -10,7 +10,8 @@ import importlib
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TypedDict, Optional
|
from typing import Dict, TypedDict, Optional
|
||||||
|
from aiohttp import web
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -257,7 +258,54 @@ comfyui-frontend-package is not installed.
|
|||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def templates_path(cls) -> str:
|
def template_asset_map(cls) -> Optional[Dict[str, str]]:
|
||||||
|
"""Return a mapping of template asset names to their absolute paths."""
|
||||||
|
try:
|
||||||
|
from comfyui_workflow_templates import (
|
||||||
|
get_asset_path,
|
||||||
|
iter_templates,
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
logging.error(
|
||||||
|
f"""
|
||||||
|
********** ERROR ***********
|
||||||
|
|
||||||
|
comfyui-workflow-templates is not installed.
|
||||||
|
|
||||||
|
{frontend_install_warning_message()}
|
||||||
|
|
||||||
|
********** ERROR ***********
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
template_entries = list(iter_templates())
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error(f"Failed to enumerate workflow templates: {exc}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
asset_map: Dict[str, str] = {}
|
||||||
|
try:
|
||||||
|
for entry in template_entries:
|
||||||
|
for asset in entry.assets:
|
||||||
|
asset_map[asset.filename] = get_asset_path(
|
||||||
|
entry.template_id, asset.filename
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error(f"Failed to resolve template asset paths: {exc}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not asset_map:
|
||||||
|
logging.error("No workflow template assets found. Did the packages install correctly?")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return asset_map
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def legacy_templates_path(cls) -> Optional[str]:
|
||||||
|
"""Return the legacy templates directory shipped inside the meta package."""
|
||||||
try:
|
try:
|
||||||
import comfyui_workflow_templates
|
import comfyui_workflow_templates
|
||||||
|
|
||||||
@ -276,6 +324,7 @@ comfyui-workflow-templates is not installed.
|
|||||||
********** ERROR ***********
|
********** ERROR ***********
|
||||||
""".strip()
|
""".strip()
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def embedded_docs_path(cls) -> str:
|
def embedded_docs_path(cls) -> str:
|
||||||
@ -392,3 +441,17 @@ comfyui-workflow-templates is not installed.
|
|||||||
logging.info("Falling back to the default frontend.")
|
logging.info("Falling back to the default frontend.")
|
||||||
check_frontend_version()
|
check_frontend_version()
|
||||||
return cls.default_frontend_path()
|
return cls.default_frontend_path()
|
||||||
|
@classmethod
|
||||||
|
def template_asset_handler(cls):
|
||||||
|
assets = cls.template_asset_map()
|
||||||
|
if not assets:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def serve_template(request: web.Request) -> web.StreamResponse:
|
||||||
|
rel_path = request.match_info.get("path", "")
|
||||||
|
target = assets.get(rel_path)
|
||||||
|
if target is None:
|
||||||
|
raise web.HTTPNotFound()
|
||||||
|
return web.FileResponse(target)
|
||||||
|
|
||||||
|
return serve_template
|
||||||
|
|||||||
@ -58,7 +58,8 @@ except (ModuleNotFoundError, TypeError):
|
|||||||
NVIDIA_MEMORY_CONV_BUG_WORKAROUND = False
|
NVIDIA_MEMORY_CONV_BUG_WORKAROUND = False
|
||||||
try:
|
try:
|
||||||
if comfy.model_management.is_nvidia():
|
if comfy.model_management.is_nvidia():
|
||||||
if torch.backends.cudnn.version() >= 91002 and comfy.model_management.torch_version_numeric >= (2, 9) and comfy.model_management.torch_version_numeric <= (2, 10):
|
cudnn_version = torch.backends.cudnn.version()
|
||||||
|
if (cudnn_version >= 91002 and cudnn_version < 91500) and comfy.model_management.torch_version_numeric >= (2, 9) and comfy.model_management.torch_version_numeric <= (2, 10):
|
||||||
#TODO: change upper bound version once it's fixed'
|
#TODO: change upper bound version once it's fixed'
|
||||||
NVIDIA_MEMORY_CONV_BUG_WORKAROUND = True
|
NVIDIA_MEMORY_CONV_BUG_WORKAROUND = True
|
||||||
logging.info("working around nvidia conv3d memory bug.")
|
logging.info("working around nvidia conv3d memory bug.")
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from comfy_api.internal.singleton import ProxiedSingleton
|
|||||||
from comfy_api.internal.async_to_sync import create_sync_class
|
from comfy_api.internal.async_to_sync import create_sync_class
|
||||||
from comfy_api.latest._input import ImageInput, AudioInput, MaskInput, LatentInput, VideoInput
|
from comfy_api.latest._input import ImageInput, AudioInput, MaskInput, LatentInput, VideoInput
|
||||||
from comfy_api.latest._input_impl import VideoFromFile, VideoFromComponents
|
from comfy_api.latest._input_impl import VideoFromFile, VideoFromComponents
|
||||||
from comfy_api.latest._util import VideoCodec, VideoContainer, VideoComponents
|
from comfy_api.latest._util import VideoCodec, VideoContainer, VideoComponents, MESH, VOXEL
|
||||||
from . import _io as io
|
from . import _io as io
|
||||||
from . import _ui as ui
|
from . import _ui as ui
|
||||||
# from comfy_api.latest._resources import _RESOURCES as resources #noqa: F401
|
# from comfy_api.latest._resources import _RESOURCES as resources #noqa: F401
|
||||||
@ -104,6 +104,8 @@ class Types:
|
|||||||
VideoCodec = VideoCodec
|
VideoCodec = VideoCodec
|
||||||
VideoContainer = VideoContainer
|
VideoContainer = VideoContainer
|
||||||
VideoComponents = VideoComponents
|
VideoComponents = VideoComponents
|
||||||
|
MESH = MESH
|
||||||
|
VOXEL = VOXEL
|
||||||
|
|
||||||
ComfyAPI = ComfyAPI_latest
|
ComfyAPI = ComfyAPI_latest
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ from comfy_api.internal import (_ComfyNodeInternal, _NodeOutputInternal, classpr
|
|||||||
prune_dict, shallow_clone_class)
|
prune_dict, shallow_clone_class)
|
||||||
from comfy_api.latest._resources import Resources, ResourcesLocal
|
from comfy_api.latest._resources import Resources, ResourcesLocal
|
||||||
from comfy_execution.graph_utils import ExecutionBlocker
|
from comfy_execution.graph_utils import ExecutionBlocker
|
||||||
|
from ._util import MESH, VOXEL
|
||||||
|
|
||||||
# from comfy_extras.nodes_images import SVG as SVG_ # NOTE: needs to be moved before can be imported due to circular reference
|
# from comfy_extras.nodes_images import SVG as SVG_ # NOTE: needs to be moved before can be imported due to circular reference
|
||||||
|
|
||||||
@ -656,11 +657,11 @@ class LossMap(ComfyTypeIO):
|
|||||||
|
|
||||||
@comfytype(io_type="VOXEL")
|
@comfytype(io_type="VOXEL")
|
||||||
class Voxel(ComfyTypeIO):
|
class Voxel(ComfyTypeIO):
|
||||||
Type = Any # TODO: VOXEL class is defined in comfy_extras/nodes_hunyuan3d.py; should be moved to somewhere else before referenced directly in v3
|
Type = VOXEL
|
||||||
|
|
||||||
@comfytype(io_type="MESH")
|
@comfytype(io_type="MESH")
|
||||||
class Mesh(ComfyTypeIO):
|
class Mesh(ComfyTypeIO):
|
||||||
Type = Any # TODO: MESH class is defined in comfy_extras/nodes_hunyuan3d.py; should be moved to somewhere else before referenced directly in v3
|
Type = MESH
|
||||||
|
|
||||||
@comfytype(io_type="HOOKS")
|
@comfytype(io_type="HOOKS")
|
||||||
class Hooks(ComfyTypeIO):
|
class Hooks(ComfyTypeIO):
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
from .video_types import VideoContainer, VideoCodec, VideoComponents
|
from .video_types import VideoContainer, VideoCodec, VideoComponents
|
||||||
|
from .geometry_types import VOXEL, MESH
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Utility Types
|
# Utility Types
|
||||||
"VideoContainer",
|
"VideoContainer",
|
||||||
"VideoCodec",
|
"VideoCodec",
|
||||||
"VideoComponents",
|
"VideoComponents",
|
||||||
|
"VOXEL",
|
||||||
|
"MESH",
|
||||||
]
|
]
|
||||||
|
|||||||
12
comfy_api/latest/_util/geometry_types.py
Normal file
12
comfy_api/latest/_util/geometry_types.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
class VOXEL:
|
||||||
|
def __init__(self, data: torch.Tensor):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
|
||||||
|
class MESH:
|
||||||
|
def __init__(self, vertices: torch.Tensor, faces: torch.Tensor):
|
||||||
|
self.vertices = vertices
|
||||||
|
self.faces = faces
|
||||||
133
comfy_api_nodes/apis/topaz_api.py
Normal file
133
comfy_api_nodes/apis/topaz_api.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ImageEnhanceRequest(BaseModel):
|
||||||
|
model: str = Field("Reimagine")
|
||||||
|
output_format: str = Field("jpeg")
|
||||||
|
subject_detection: str = Field("All")
|
||||||
|
face_enhancement: bool = Field(True)
|
||||||
|
face_enhancement_creativity: float = Field(0, description="Is ignored if face_enhancement is false")
|
||||||
|
face_enhancement_strength: float = Field(0.8, description="Is ignored if face_enhancement is false")
|
||||||
|
source_url: str = Field(...)
|
||||||
|
output_width: Optional[int] = Field(None)
|
||||||
|
output_height: Optional[int] = Field(None)
|
||||||
|
crop_to_fill: bool = Field(False)
|
||||||
|
prompt: Optional[str] = Field(None, description="Text prompt for creative upscaling guidance")
|
||||||
|
creativity: int = Field(3, description="Creativity settings range from 1 to 9")
|
||||||
|
face_preservation: str = Field("true", description="To preserve the identity of characters")
|
||||||
|
color_preservation: str = Field("true", description="To preserve the original color")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAsyncTaskResponse(BaseModel):
|
||||||
|
process_id: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageStatusResponse(BaseModel):
|
||||||
|
process_id: str = Field(...)
|
||||||
|
status: str = Field(...)
|
||||||
|
progress: Optional[int] = Field(None)
|
||||||
|
credits: int = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageDownloadResponse(BaseModel):
|
||||||
|
download_url: str = Field(...)
|
||||||
|
expiry: int = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class Resolution(BaseModel):
|
||||||
|
width: int = Field(...)
|
||||||
|
height: int = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCreateVideoRequestSource(BaseModel):
|
||||||
|
container: str = Field(...)
|
||||||
|
size: int = Field(..., description="Size of the video file in bytes")
|
||||||
|
duration: int = Field(..., description="Duration of the video file in seconds")
|
||||||
|
frameCount: int = Field(..., description="Total number of frames in the video")
|
||||||
|
frameRate: int = Field(...)
|
||||||
|
resolution: Resolution = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoFrameInterpolationFilter(BaseModel):
|
||||||
|
model: str = Field(...)
|
||||||
|
slowmo: Optional[int] = Field(None)
|
||||||
|
fps: int = Field(...)
|
||||||
|
duplicate: bool = Field(...)
|
||||||
|
duplicate_threshold: float = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoEnhancementFilter(BaseModel):
|
||||||
|
model: str = Field(...)
|
||||||
|
auto: Optional[str] = Field(None, description="Auto, Manual, Relative")
|
||||||
|
focusFixLevel: Optional[str] = Field(None, description="Downscales video input for correction of blurred subjects")
|
||||||
|
compression: Optional[float] = Field(None, description="Strength of compression recovery")
|
||||||
|
details: Optional[float] = Field(None, description="Amount of detail reconstruction")
|
||||||
|
prenoise: Optional[float] = Field(None, description="Amount of noise to add to input to reduce over-smoothing")
|
||||||
|
noise: Optional[float] = Field(None, description="Amount of noise reduction")
|
||||||
|
halo: Optional[float] = Field(None, description="Amount of halo reduction")
|
||||||
|
preblur: Optional[float] = Field(None, description="Anti-aliasing and deblurring strength")
|
||||||
|
blur: Optional[float] = Field(None, description="Amount of sharpness applied")
|
||||||
|
grain: Optional[float] = Field(None, description="Grain after AI model processing")
|
||||||
|
grainSize: Optional[float] = Field(None, description="Size of generated grain")
|
||||||
|
recoverOriginalDetailValue: Optional[float] = Field(None, description="Source details into the output video")
|
||||||
|
creativity: Optional[str] = Field(None, description="Creativity level(high, low) for slc-1 only")
|
||||||
|
isOptimizedMode: Optional[bool] = Field(None, description="Set to true for Starlight Creative (slc-1) only")
|
||||||
|
|
||||||
|
|
||||||
|
class OutputInformationVideo(BaseModel):
|
||||||
|
resolution: Resolution = Field(...)
|
||||||
|
frameRate: int = Field(...)
|
||||||
|
audioCodec: Optional[str] = Field(..., description="Required if audioTransfer is Copy or Convert")
|
||||||
|
audioTransfer: str = Field(..., description="Copy, Convert, None")
|
||||||
|
dynamicCompressionLevel: str = Field(..., description="Low, Mid, High")
|
||||||
|
|
||||||
|
|
||||||
|
class Overrides(BaseModel):
|
||||||
|
isPaidDiffusion: bool = Field(True)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateVideoRequest(BaseModel):
|
||||||
|
source: CreateCreateVideoRequestSource = Field(...)
|
||||||
|
filters: list[Union[VideoFrameInterpolationFilter, VideoEnhancementFilter]] = Field(...)
|
||||||
|
output: OutputInformationVideo = Field(...)
|
||||||
|
overrides: Overrides = Field(Overrides(isPaidDiffusion=True))
|
||||||
|
|
||||||
|
|
||||||
|
class CreateVideoResponse(BaseModel):
|
||||||
|
requestId: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoAcceptResponse(BaseModel):
|
||||||
|
uploadId: str = Field(...)
|
||||||
|
urls: list[str] = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoCompleteUploadRequestPart(BaseModel):
|
||||||
|
partNum: int = Field(...)
|
||||||
|
eTag: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoCompleteUploadRequest(BaseModel):
|
||||||
|
uploadResults: list[VideoCompleteUploadRequestPart] = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoCompleteUploadResponse(BaseModel):
|
||||||
|
message: str = Field(..., description="Confirmation message")
|
||||||
|
|
||||||
|
|
||||||
|
class VideoStatusResponseEstimates(BaseModel):
|
||||||
|
cost: list[int] = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoStatusResponseDownloadUrl(BaseModel):
|
||||||
|
url: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoStatusResponse(BaseModel):
|
||||||
|
status: str = Field(...)
|
||||||
|
estimates: Optional[VideoStatusResponseEstimates] = Field(None)
|
||||||
|
progress: Optional[float] = Field(None)
|
||||||
|
message: Optional[str] = Field("")
|
||||||
|
download: Optional[VideoStatusResponseDownloadUrl] = Field(None)
|
||||||
421
comfy_api_nodes/nodes_topaz.py
Normal file
421
comfy_api_nodes/nodes_topaz.py
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
import builtins
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import torch
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from comfy_api.input.video_types import VideoInput
|
||||||
|
from comfy_api.latest import IO, ComfyExtension
|
||||||
|
from comfy_api_nodes.apis import topaz_api
|
||||||
|
from comfy_api_nodes.util import (
|
||||||
|
ApiEndpoint,
|
||||||
|
download_url_to_image_tensor,
|
||||||
|
download_url_to_video_output,
|
||||||
|
get_fs_object_size,
|
||||||
|
get_number_of_images,
|
||||||
|
poll_op,
|
||||||
|
sync_op,
|
||||||
|
upload_images_to_comfyapi,
|
||||||
|
validate_container_format_is_mp4,
|
||||||
|
)
|
||||||
|
|
||||||
|
UPSCALER_MODELS_MAP = {
|
||||||
|
"Starlight (Astra) Fast": "slf-1",
|
||||||
|
"Starlight (Astra) Creative": "slc-1",
|
||||||
|
}
|
||||||
|
UPSCALER_VALUES_MAP = {
|
||||||
|
"FullHD (1080p)": 1920,
|
||||||
|
"4K (2160p)": 3840,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TopazImageEnhance(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="TopazImageEnhance",
|
||||||
|
display_name="Topaz Image Enhance",
|
||||||
|
category="api node/image/Topaz",
|
||||||
|
description="Industry-standard upscaling and image enhancement.",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input("model", options=["Reimagine"]),
|
||||||
|
IO.Image.Input("image"),
|
||||||
|
IO.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="Optional text prompt for creative upscaling guidance.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"subject_detection",
|
||||||
|
options=["All", "Foreground", "Background"],
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"face_enhancement",
|
||||||
|
default=True,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Enhance faces (if present) during processing.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"face_enhancement_creativity",
|
||||||
|
default=0.0,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Set the creativity level for face enhancement.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"face_enhancement_strength",
|
||||||
|
default=1.0,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Controls how sharp enhanced faces are relative to the background.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"crop_to_fill",
|
||||||
|
default=False,
|
||||||
|
optional=True,
|
||||||
|
tooltip="By default, the image is letterboxed when the output aspect ratio differs. "
|
||||||
|
"Enable to crop the image to fill the output dimensions.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"output_width",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=32000,
|
||||||
|
step=1,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Zero value means to calculate automatically (usually it will be original size or output_height if specified).",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"output_height",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=32000,
|
||||||
|
step=1,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Zero value means to output in the same height as original or output width.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"creativity",
|
||||||
|
default=3,
|
||||||
|
min=1,
|
||||||
|
max=9,
|
||||||
|
step=1,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"face_preservation",
|
||||||
|
default=True,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Preserve subjects' facial identity.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"color_preservation",
|
||||||
|
default=True,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Preserve the original colors.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Image.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model: str,
|
||||||
|
image: torch.Tensor,
|
||||||
|
prompt: str = "",
|
||||||
|
subject_detection: str = "All",
|
||||||
|
face_enhancement: bool = True,
|
||||||
|
face_enhancement_creativity: float = 1.0,
|
||||||
|
face_enhancement_strength: float = 0.8,
|
||||||
|
crop_to_fill: bool = False,
|
||||||
|
output_width: int = 0,
|
||||||
|
output_height: int = 0,
|
||||||
|
creativity: int = 3,
|
||||||
|
face_preservation: bool = True,
|
||||||
|
color_preservation: bool = True,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
if get_number_of_images(image) != 1:
|
||||||
|
raise ValueError("Only one input image is supported.")
|
||||||
|
download_url = await upload_images_to_comfyapi(cls, image, max_images=1, mime_type="image/png")
|
||||||
|
initial_response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/topaz/image/v1/enhance-gen/async", method="POST"),
|
||||||
|
response_model=topaz_api.ImageAsyncTaskResponse,
|
||||||
|
data=topaz_api.ImageEnhanceRequest(
|
||||||
|
model=model,
|
||||||
|
prompt=prompt,
|
||||||
|
subject_detection=subject_detection,
|
||||||
|
face_enhancement=face_enhancement,
|
||||||
|
face_enhancement_creativity=face_enhancement_creativity,
|
||||||
|
face_enhancement_strength=face_enhancement_strength,
|
||||||
|
crop_to_fill=crop_to_fill,
|
||||||
|
output_width=output_width if output_width else None,
|
||||||
|
output_height=output_height if output_height else None,
|
||||||
|
creativity=creativity,
|
||||||
|
face_preservation=str(face_preservation).lower(),
|
||||||
|
color_preservation=str(color_preservation).lower(),
|
||||||
|
source_url=download_url[0],
|
||||||
|
output_format="png",
|
||||||
|
),
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
)
|
||||||
|
|
||||||
|
await poll_op(
|
||||||
|
cls,
|
||||||
|
poll_endpoint=ApiEndpoint(path=f"/proxy/topaz/image/v1/status/{initial_response.process_id}"),
|
||||||
|
response_model=topaz_api.ImageStatusResponse,
|
||||||
|
status_extractor=lambda x: x.status,
|
||||||
|
progress_extractor=lambda x: getattr(x, "progress", 0),
|
||||||
|
price_extractor=lambda x: x.credits * 0.08,
|
||||||
|
poll_interval=8.0,
|
||||||
|
max_poll_attempts=160,
|
||||||
|
estimated_duration=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
results = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/topaz/image/v1/download/{initial_response.process_id}"),
|
||||||
|
response_model=topaz_api.ImageDownloadResponse,
|
||||||
|
monitor_progress=False,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(await download_url_to_image_tensor(results.download_url))
|
||||||
|
|
||||||
|
|
||||||
|
class TopazVideoEnhance(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="TopazVideoEnhance",
|
||||||
|
display_name="Topaz Video Enhance",
|
||||||
|
category="api node/video/Topaz",
|
||||||
|
description="Breathe new life into video with powerful upscaling and recovery technology.",
|
||||||
|
inputs=[
|
||||||
|
IO.Video.Input("video"),
|
||||||
|
IO.Boolean.Input("upscaler_enabled", default=True),
|
||||||
|
IO.Combo.Input("upscaler_model", options=list(UPSCALER_MODELS_MAP.keys())),
|
||||||
|
IO.Combo.Input("upscaler_resolution", options=list(UPSCALER_VALUES_MAP.keys())),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"upscaler_creativity",
|
||||||
|
options=["low", "middle", "high"],
|
||||||
|
default="low",
|
||||||
|
tooltip="Creativity level (applies only to Starlight (Astra) Creative).",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
IO.Boolean.Input("interpolation_enabled", default=False, optional=True),
|
||||||
|
IO.Combo.Input("interpolation_model", options=["apo-8"], default="apo-8", optional=True),
|
||||||
|
IO.Int.Input(
|
||||||
|
"interpolation_slowmo",
|
||||||
|
default=1,
|
||||||
|
min=1,
|
||||||
|
max=16,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
tooltip="Slow-motion factor applied to the input video. "
|
||||||
|
"For example, 2 makes the output twice as slow and doubles the duration.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"interpolation_frame_rate",
|
||||||
|
default=60,
|
||||||
|
min=15,
|
||||||
|
max=240,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
tooltip="Output frame rate.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"interpolation_duplicate",
|
||||||
|
default=False,
|
||||||
|
tooltip="Analyze the input for duplicate frames and remove them.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"interpolation_duplicate_threshold",
|
||||||
|
default=0.01,
|
||||||
|
min=0.001,
|
||||||
|
max=0.1,
|
||||||
|
step=0.001,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
tooltip="Detection sensitivity for duplicate frames.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"dynamic_compression_level",
|
||||||
|
options=["Low", "Mid", "High"],
|
||||||
|
default="Low",
|
||||||
|
tooltip="CQP level.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Video.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
video: VideoInput,
|
||||||
|
upscaler_enabled: bool,
|
||||||
|
upscaler_model: str,
|
||||||
|
upscaler_resolution: str,
|
||||||
|
upscaler_creativity: str = "low",
|
||||||
|
interpolation_enabled: bool = False,
|
||||||
|
interpolation_model: str = "apo-8",
|
||||||
|
interpolation_slowmo: int = 1,
|
||||||
|
interpolation_frame_rate: int = 60,
|
||||||
|
interpolation_duplicate: bool = False,
|
||||||
|
interpolation_duplicate_threshold: float = 0.01,
|
||||||
|
dynamic_compression_level: str = "Low",
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
if upscaler_enabled is False and interpolation_enabled is False:
|
||||||
|
raise ValueError("There is nothing to do: both upscaling and interpolation are disabled.")
|
||||||
|
src_width, src_height = video.get_dimensions()
|
||||||
|
video_components = video.get_components()
|
||||||
|
src_frame_rate = int(video_components.frame_rate)
|
||||||
|
duration_sec = video.get_duration()
|
||||||
|
estimated_frames = int(duration_sec * src_frame_rate)
|
||||||
|
validate_container_format_is_mp4(video)
|
||||||
|
src_video_stream = video.get_stream_source()
|
||||||
|
target_width = src_width
|
||||||
|
target_height = src_height
|
||||||
|
target_frame_rate = src_frame_rate
|
||||||
|
filters = []
|
||||||
|
if upscaler_enabled:
|
||||||
|
target_width = UPSCALER_VALUES_MAP[upscaler_resolution]
|
||||||
|
target_height = UPSCALER_VALUES_MAP[upscaler_resolution]
|
||||||
|
filters.append(
|
||||||
|
topaz_api.VideoEnhancementFilter(
|
||||||
|
model=UPSCALER_MODELS_MAP[upscaler_model],
|
||||||
|
creativity=(upscaler_creativity if UPSCALER_MODELS_MAP[upscaler_model] == "slc-1" else None),
|
||||||
|
isOptimizedMode=(True if UPSCALER_MODELS_MAP[upscaler_model] == "slc-1" else None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if interpolation_enabled:
|
||||||
|
target_frame_rate = interpolation_frame_rate
|
||||||
|
filters.append(
|
||||||
|
topaz_api.VideoFrameInterpolationFilter(
|
||||||
|
model=interpolation_model,
|
||||||
|
slowmo=interpolation_slowmo,
|
||||||
|
fps=interpolation_frame_rate,
|
||||||
|
duplicate=interpolation_duplicate,
|
||||||
|
duplicate_threshold=interpolation_duplicate_threshold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
initial_res = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/topaz/video/", method="POST"),
|
||||||
|
response_model=topaz_api.CreateVideoResponse,
|
||||||
|
data=topaz_api.CreateVideoRequest(
|
||||||
|
source=topaz_api.CreateCreateVideoRequestSource(
|
||||||
|
container="mp4",
|
||||||
|
size=get_fs_object_size(src_video_stream),
|
||||||
|
duration=int(duration_sec),
|
||||||
|
frameCount=estimated_frames,
|
||||||
|
frameRate=src_frame_rate,
|
||||||
|
resolution=topaz_api.Resolution(width=src_width, height=src_height),
|
||||||
|
),
|
||||||
|
filters=filters,
|
||||||
|
output=topaz_api.OutputInformationVideo(
|
||||||
|
resolution=topaz_api.Resolution(width=target_width, height=target_height),
|
||||||
|
frameRate=target_frame_rate,
|
||||||
|
audioCodec="AAC",
|
||||||
|
audioTransfer="Copy",
|
||||||
|
dynamicCompressionLevel=dynamic_compression_level,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
wait_label="Creating task",
|
||||||
|
final_label_on_success="Task created",
|
||||||
|
)
|
||||||
|
upload_res = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(
|
||||||
|
path=f"/proxy/topaz/video/{initial_res.requestId}/accept",
|
||||||
|
method="PATCH",
|
||||||
|
),
|
||||||
|
response_model=topaz_api.VideoAcceptResponse,
|
||||||
|
wait_label="Preparing upload",
|
||||||
|
final_label_on_success="Upload started",
|
||||||
|
)
|
||||||
|
if len(upload_res.urls) > 1:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Large files are not currently supported. Please open an issue in the ComfyUI repository."
|
||||||
|
)
|
||||||
|
async with aiohttp.ClientSession(headers={"Content-Type": "video/mp4"}) as session:
|
||||||
|
if isinstance(src_video_stream, BytesIO):
|
||||||
|
src_video_stream.seek(0)
|
||||||
|
async with session.put(upload_res.urls[0], data=src_video_stream, raise_for_status=True) as res:
|
||||||
|
upload_etag = res.headers["Etag"]
|
||||||
|
else:
|
||||||
|
with builtins.open(src_video_stream, "rb") as video_file:
|
||||||
|
async with session.put(upload_res.urls[0], data=video_file, raise_for_status=True) as res:
|
||||||
|
upload_etag = res.headers["Etag"]
|
||||||
|
await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(
|
||||||
|
path=f"/proxy/topaz/video/{initial_res.requestId}/complete-upload",
|
||||||
|
method="PATCH",
|
||||||
|
),
|
||||||
|
response_model=topaz_api.VideoCompleteUploadResponse,
|
||||||
|
data=topaz_api.VideoCompleteUploadRequest(
|
||||||
|
uploadResults=[
|
||||||
|
topaz_api.VideoCompleteUploadRequestPart(
|
||||||
|
partNum=1,
|
||||||
|
eTag=upload_etag,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
wait_label="Finalizing upload",
|
||||||
|
final_label_on_success="Upload completed",
|
||||||
|
)
|
||||||
|
final_response = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/topaz/video/{initial_res.requestId}/status"),
|
||||||
|
response_model=topaz_api.VideoStatusResponse,
|
||||||
|
status_extractor=lambda x: x.status,
|
||||||
|
progress_extractor=lambda x: getattr(x, "progress", 0),
|
||||||
|
price_extractor=lambda x: (x.estimates.cost[0] * 0.08 if x.estimates and x.estimates.cost[0] else None),
|
||||||
|
poll_interval=10.0,
|
||||||
|
max_poll_attempts=320,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(await download_url_to_video_output(final_response.download.url))
|
||||||
|
|
||||||
|
|
||||||
|
class TopazExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
TopazImageEnhance,
|
||||||
|
TopazVideoEnhance,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> TopazExtension:
|
||||||
|
return TopazExtension()
|
||||||
@ -77,9 +77,9 @@ class _PollUIState:
|
|||||||
|
|
||||||
|
|
||||||
_RETRY_STATUS = {408, 429, 500, 502, 503, 504}
|
_RETRY_STATUS = {408, 429, 500, 502, 503, 504}
|
||||||
COMPLETED_STATUSES = ["succeeded", "succeed", "success", "completed", "finished", "done"]
|
COMPLETED_STATUSES = ["succeeded", "succeed", "success", "completed", "finished", "done", "complete"]
|
||||||
FAILED_STATUSES = ["cancelled", "canceled", "fail", "failed", "error"]
|
FAILED_STATUSES = ["cancelled", "canceled", "canceling", "fail", "failed", "error"]
|
||||||
QUEUED_STATUSES = ["created", "queued", "queueing", "submitted"]
|
QUEUED_STATUSES = ["created", "queued", "queueing", "submitted", "initializing"]
|
||||||
|
|
||||||
|
|
||||||
async def sync_op(
|
async def sync_op(
|
||||||
@ -424,7 +424,8 @@ def _display_text(
|
|||||||
if status:
|
if status:
|
||||||
display_lines.append(f"Status: {status.capitalize() if isinstance(status, str) else status}")
|
display_lines.append(f"Status: {status.capitalize() if isinstance(status, str) else status}")
|
||||||
if price is not None:
|
if price is not None:
|
||||||
display_lines.append(f"Price: ${float(price):,.4f}")
|
p = f"{float(price):,.4f}".rstrip("0").rstrip(".")
|
||||||
|
display_lines.append(f"Price: ${p}")
|
||||||
if text is not None:
|
if text is not None:
|
||||||
display_lines.append(text)
|
display_lines.append(text)
|
||||||
if display_lines:
|
if display_lines:
|
||||||
|
|||||||
@ -7,63 +7,79 @@ from comfy.ldm.modules.diffusionmodules.mmdit import get_1d_sincos_pos_embed_fro
|
|||||||
import folder_paths
|
import folder_paths
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
|
from typing_extensions import override
|
||||||
|
from comfy_api.latest import ComfyExtension, IO, Types
|
||||||
|
from comfy_api.latest._util import MESH, VOXEL # only for backward compatibility if someone import it from this file (will be removed later) # noqa
|
||||||
|
|
||||||
class EmptyLatentHunyuan3Dv2:
|
|
||||||
|
class EmptyLatentHunyuan3Dv2(IO.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {
|
return IO.Schema(
|
||||||
"required": {
|
node_id="EmptyLatentHunyuan3Dv2",
|
||||||
"resolution": ("INT", {"default": 3072, "min": 1, "max": 8192}),
|
category="latent/3d",
|
||||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."}),
|
inputs=[
|
||||||
}
|
IO.Int.Input("resolution", default=3072, min=1, max=8192),
|
||||||
}
|
IO.Int.Input("batch_size", default=1, min=1, max=4096, tooltip="The number of latent images in the batch."),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Latent.Output(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("LATENT",)
|
@classmethod
|
||||||
FUNCTION = "generate"
|
def execute(cls, resolution, batch_size) -> IO.NodeOutput:
|
||||||
|
|
||||||
CATEGORY = "latent/3d"
|
|
||||||
|
|
||||||
def generate(self, resolution, batch_size):
|
|
||||||
latent = torch.zeros([batch_size, 64, resolution], device=comfy.model_management.intermediate_device())
|
latent = torch.zeros([batch_size, 64, resolution], device=comfy.model_management.intermediate_device())
|
||||||
return ({"samples": latent, "type": "hunyuan3dv2"}, )
|
return IO.NodeOutput({"samples": latent, "type": "hunyuan3dv2"})
|
||||||
|
|
||||||
class Hunyuan3Dv2Conditioning:
|
generate = execute # TODO: remove
|
||||||
|
|
||||||
|
|
||||||
|
class Hunyuan3Dv2Conditioning(IO.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {"clip_vision_output": ("CLIP_VISION_OUTPUT",),
|
return IO.Schema(
|
||||||
}}
|
node_id="Hunyuan3Dv2Conditioning",
|
||||||
|
category="conditioning/video_models",
|
||||||
|
inputs=[
|
||||||
|
IO.ClipVisionOutput.Input("clip_vision_output"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Conditioning.Output(display_name="positive"),
|
||||||
|
IO.Conditioning.Output(display_name="negative"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("CONDITIONING", "CONDITIONING")
|
@classmethod
|
||||||
RETURN_NAMES = ("positive", "negative")
|
def execute(cls, clip_vision_output) -> IO.NodeOutput:
|
||||||
|
|
||||||
FUNCTION = "encode"
|
|
||||||
|
|
||||||
CATEGORY = "conditioning/video_models"
|
|
||||||
|
|
||||||
def encode(self, clip_vision_output):
|
|
||||||
embeds = clip_vision_output.last_hidden_state
|
embeds = clip_vision_output.last_hidden_state
|
||||||
positive = [[embeds, {}]]
|
positive = [[embeds, {}]]
|
||||||
negative = [[torch.zeros_like(embeds), {}]]
|
negative = [[torch.zeros_like(embeds), {}]]
|
||||||
return (positive, negative)
|
return IO.NodeOutput(positive, negative)
|
||||||
|
|
||||||
|
encode = execute # TODO: remove
|
||||||
|
|
||||||
|
|
||||||
class Hunyuan3Dv2ConditioningMultiView:
|
class Hunyuan3Dv2ConditioningMultiView(IO.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {},
|
return IO.Schema(
|
||||||
"optional": {"front": ("CLIP_VISION_OUTPUT",),
|
node_id="Hunyuan3Dv2ConditioningMultiView",
|
||||||
"left": ("CLIP_VISION_OUTPUT",),
|
category="conditioning/video_models",
|
||||||
"back": ("CLIP_VISION_OUTPUT",),
|
inputs=[
|
||||||
"right": ("CLIP_VISION_OUTPUT",), }}
|
IO.ClipVisionOutput.Input("front", optional=True),
|
||||||
|
IO.ClipVisionOutput.Input("left", optional=True),
|
||||||
|
IO.ClipVisionOutput.Input("back", optional=True),
|
||||||
|
IO.ClipVisionOutput.Input("right", optional=True),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Conditioning.Output(display_name="positive"),
|
||||||
|
IO.Conditioning.Output(display_name="negative"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("CONDITIONING", "CONDITIONING")
|
@classmethod
|
||||||
RETURN_NAMES = ("positive", "negative")
|
def execute(cls, front=None, left=None, back=None, right=None) -> IO.NodeOutput:
|
||||||
|
|
||||||
FUNCTION = "encode"
|
|
||||||
|
|
||||||
CATEGORY = "conditioning/video_models"
|
|
||||||
|
|
||||||
def encode(self, front=None, left=None, back=None, right=None):
|
|
||||||
all_embeds = [front, left, back, right]
|
all_embeds = [front, left, back, right]
|
||||||
out = []
|
out = []
|
||||||
pos_embeds = None
|
pos_embeds = None
|
||||||
@ -76,29 +92,35 @@ class Hunyuan3Dv2ConditioningMultiView:
|
|||||||
embeds = torch.cat(out, dim=1)
|
embeds = torch.cat(out, dim=1)
|
||||||
positive = [[embeds, {}]]
|
positive = [[embeds, {}]]
|
||||||
negative = [[torch.zeros_like(embeds), {}]]
|
negative = [[torch.zeros_like(embeds), {}]]
|
||||||
return (positive, negative)
|
return IO.NodeOutput(positive, negative)
|
||||||
|
|
||||||
|
encode = execute # TODO: remove
|
||||||
|
|
||||||
|
|
||||||
class VOXEL:
|
class VAEDecodeHunyuan3D(IO.ComfyNode):
|
||||||
def __init__(self, data):
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
class VAEDecodeHunyuan3D:
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {"samples": ("LATENT", ),
|
return IO.Schema(
|
||||||
"vae": ("VAE", ),
|
node_id="VAEDecodeHunyuan3D",
|
||||||
"num_chunks": ("INT", {"default": 8000, "min": 1000, "max": 500000}),
|
category="latent/3d",
|
||||||
"octree_resolution": ("INT", {"default": 256, "min": 16, "max": 512}),
|
inputs=[
|
||||||
}}
|
IO.Latent.Input("samples"),
|
||||||
RETURN_TYPES = ("VOXEL",)
|
IO.Vae.Input("vae"),
|
||||||
FUNCTION = "decode"
|
IO.Int.Input("num_chunks", default=8000, min=1000, max=500000),
|
||||||
|
IO.Int.Input("octree_resolution", default=256, min=16, max=512),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Voxel.Output(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "latent/3d"
|
@classmethod
|
||||||
|
def execute(cls, vae, samples, num_chunks, octree_resolution) -> IO.NodeOutput:
|
||||||
|
voxels = Types.VOXEL(vae.decode(samples["samples"], vae_options={"num_chunks": num_chunks, "octree_resolution": octree_resolution}))
|
||||||
|
return IO.NodeOutput(voxels)
|
||||||
|
|
||||||
|
decode = execute # TODO: remove
|
||||||
|
|
||||||
def decode(self, vae, samples, num_chunks, octree_resolution):
|
|
||||||
voxels = VOXEL(vae.decode(samples["samples"], vae_options={"num_chunks": num_chunks, "octree_resolution": octree_resolution}))
|
|
||||||
return (voxels, )
|
|
||||||
|
|
||||||
def voxel_to_mesh(voxels, threshold=0.5, device=None):
|
def voxel_to_mesh(voxels, threshold=0.5, device=None):
|
||||||
if device is None:
|
if device is None:
|
||||||
@ -396,24 +418,24 @@ def voxel_to_mesh_surfnet(voxels, threshold=0.5, device=None):
|
|||||||
|
|
||||||
return final_vertices, faces
|
return final_vertices, faces
|
||||||
|
|
||||||
class MESH:
|
|
||||||
def __init__(self, vertices, faces):
|
|
||||||
self.vertices = vertices
|
|
||||||
self.faces = faces
|
|
||||||
|
|
||||||
|
class VoxelToMeshBasic(IO.ComfyNode):
|
||||||
class VoxelToMeshBasic:
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {"voxel": ("VOXEL", ),
|
return IO.Schema(
|
||||||
"threshold": ("FLOAT", {"default": 0.6, "min": -1.0, "max": 1.0, "step": 0.01}),
|
node_id="VoxelToMeshBasic",
|
||||||
}}
|
category="3d",
|
||||||
RETURN_TYPES = ("MESH",)
|
inputs=[
|
||||||
FUNCTION = "decode"
|
IO.Voxel.Input("voxel"),
|
||||||
|
IO.Float.Input("threshold", default=0.6, min=-1.0, max=1.0, step=0.01),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Mesh.Output(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "3d"
|
@classmethod
|
||||||
|
def execute(cls, voxel, threshold) -> IO.NodeOutput:
|
||||||
def decode(self, voxel, threshold):
|
|
||||||
vertices = []
|
vertices = []
|
||||||
faces = []
|
faces = []
|
||||||
for x in voxel.data:
|
for x in voxel.data:
|
||||||
@ -421,21 +443,29 @@ class VoxelToMeshBasic:
|
|||||||
vertices.append(v)
|
vertices.append(v)
|
||||||
faces.append(f)
|
faces.append(f)
|
||||||
|
|
||||||
return (MESH(torch.stack(vertices), torch.stack(faces)), )
|
return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces)))
|
||||||
|
|
||||||
class VoxelToMesh:
|
decode = execute # TODO: remove
|
||||||
|
|
||||||
|
|
||||||
|
class VoxelToMesh(IO.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {"voxel": ("VOXEL", ),
|
return IO.Schema(
|
||||||
"algorithm": (["surface net", "basic"], ),
|
node_id="VoxelToMesh",
|
||||||
"threshold": ("FLOAT", {"default": 0.6, "min": -1.0, "max": 1.0, "step": 0.01}),
|
category="3d",
|
||||||
}}
|
inputs=[
|
||||||
RETURN_TYPES = ("MESH",)
|
IO.Voxel.Input("voxel"),
|
||||||
FUNCTION = "decode"
|
IO.Combo.Input("algorithm", options=["surface net", "basic"]),
|
||||||
|
IO.Float.Input("threshold", default=0.6, min=-1.0, max=1.0, step=0.01),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Mesh.Output(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "3d"
|
@classmethod
|
||||||
|
def execute(cls, voxel, algorithm, threshold) -> IO.NodeOutput:
|
||||||
def decode(self, voxel, algorithm, threshold):
|
|
||||||
vertices = []
|
vertices = []
|
||||||
faces = []
|
faces = []
|
||||||
|
|
||||||
@ -449,7 +479,9 @@ class VoxelToMesh:
|
|||||||
vertices.append(v)
|
vertices.append(v)
|
||||||
faces.append(f)
|
faces.append(f)
|
||||||
|
|
||||||
return (MESH(torch.stack(vertices), torch.stack(faces)), )
|
return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces)))
|
||||||
|
|
||||||
|
decode = execute # TODO: remove
|
||||||
|
|
||||||
|
|
||||||
def save_glb(vertices, faces, filepath, metadata=None):
|
def save_glb(vertices, faces, filepath, metadata=None):
|
||||||
@ -581,31 +613,32 @@ def save_glb(vertices, faces, filepath, metadata=None):
|
|||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
class SaveGLB:
|
class SaveGLB(IO.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {"mesh": ("MESH", ),
|
return IO.Schema(
|
||||||
"filename_prefix": ("STRING", {"default": "mesh/ComfyUI"}), },
|
node_id="SaveGLB",
|
||||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, }
|
category="3d",
|
||||||
|
is_output_node=True,
|
||||||
|
inputs=[
|
||||||
|
IO.Mesh.Input("mesh"),
|
||||||
|
IO.String.Input("filename_prefix", default="mesh/ComfyUI"),
|
||||||
|
],
|
||||||
|
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo]
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ()
|
@classmethod
|
||||||
FUNCTION = "save"
|
def execute(cls, mesh, filename_prefix) -> IO.NodeOutput:
|
||||||
|
|
||||||
OUTPUT_NODE = True
|
|
||||||
|
|
||||||
CATEGORY = "3d"
|
|
||||||
|
|
||||||
def save(self, mesh, filename_prefix, prompt=None, extra_pnginfo=None):
|
|
||||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory())
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory())
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
metadata = {}
|
metadata = {}
|
||||||
if not args.disable_metadata:
|
if not args.disable_metadata:
|
||||||
if prompt is not None:
|
if cls.hidden.prompt is not None:
|
||||||
metadata["prompt"] = json.dumps(prompt)
|
metadata["prompt"] = json.dumps(cls.hidden.prompt)
|
||||||
if extra_pnginfo is not None:
|
if cls.hidden.extra_pnginfo is not None:
|
||||||
for x in extra_pnginfo:
|
for x in cls.hidden.extra_pnginfo:
|
||||||
metadata[x] = json.dumps(extra_pnginfo[x])
|
metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
|
||||||
|
|
||||||
for i in range(mesh.vertices.shape[0]):
|
for i in range(mesh.vertices.shape[0]):
|
||||||
f = f"{filename}_{counter:05}_.glb"
|
f = f"{filename}_{counter:05}_.glb"
|
||||||
@ -616,15 +649,22 @@ class SaveGLB:
|
|||||||
"type": "output"
|
"type": "output"
|
||||||
})
|
})
|
||||||
counter += 1
|
counter += 1
|
||||||
return {"ui": {"3d": results}}
|
return IO.NodeOutput(ui={"3d": results})
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
class Hunyuan3dExtension(ComfyExtension):
|
||||||
"EmptyLatentHunyuan3Dv2": EmptyLatentHunyuan3Dv2,
|
@override
|
||||||
"Hunyuan3Dv2Conditioning": Hunyuan3Dv2Conditioning,
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
"Hunyuan3Dv2ConditioningMultiView": Hunyuan3Dv2ConditioningMultiView,
|
return [
|
||||||
"VAEDecodeHunyuan3D": VAEDecodeHunyuan3D,
|
EmptyLatentHunyuan3Dv2,
|
||||||
"VoxelToMeshBasic": VoxelToMeshBasic,
|
Hunyuan3Dv2Conditioning,
|
||||||
"VoxelToMesh": VoxelToMesh,
|
Hunyuan3Dv2ConditioningMultiView,
|
||||||
"SaveGLB": SaveGLB,
|
VAEDecodeHunyuan3D,
|
||||||
}
|
VoxelToMeshBasic,
|
||||||
|
VoxelToMesh,
|
||||||
|
SaveGLB,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> Hunyuan3dExtension:
|
||||||
|
return Hunyuan3dExtension()
|
||||||
|
|||||||
@ -39,5 +39,5 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"PreviewAny": "Preview Any",
|
"PreviewAny": "Preview as Text",
|
||||||
}
|
}
|
||||||
|
|||||||
1
nodes.py
1
nodes.py
@ -2359,6 +2359,7 @@ async def init_builtin_api_nodes():
|
|||||||
"nodes_pika.py",
|
"nodes_pika.py",
|
||||||
"nodes_runway.py",
|
"nodes_runway.py",
|
||||||
"nodes_sora.py",
|
"nodes_sora.py",
|
||||||
|
"nodes_topaz.py",
|
||||||
"nodes_tripo.py",
|
"nodes_tripo.py",
|
||||||
"nodes_moonvalley.py",
|
"nodes_moonvalley.py",
|
||||||
"nodes_rodin.py",
|
"nodes_rodin.py",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
comfyui-frontend-package==1.28.8
|
comfyui-frontend-package==1.28.8
|
||||||
comfyui-workflow-templates==0.2.11
|
comfyui-workflow-templates==0.3.1
|
||||||
comfyui-embedded-docs==0.3.1
|
comfyui-embedded-docs==0.3.1
|
||||||
torch
|
torch
|
||||||
torchsde
|
torchsde
|
||||||
|
|||||||
32
server.py
32
server.py
@ -30,7 +30,7 @@ import comfy.model_management
|
|||||||
from comfy_api import feature_flags
|
from comfy_api import feature_flags
|
||||||
import node_helpers
|
import node_helpers
|
||||||
from comfyui_version import __version__
|
from comfyui_version import __version__
|
||||||
from app.frontend_management import FrontendManager
|
from app.frontend_management import FrontendManager, parse_version
|
||||||
from comfy_api.internal import _ComfyNodeInternal
|
from comfy_api.internal import _ComfyNodeInternal
|
||||||
|
|
||||||
from app.user_manager import UserManager
|
from app.user_manager import UserManager
|
||||||
@ -849,11 +849,31 @@ class PromptServer():
|
|||||||
for name, dir in nodes.EXTENSION_WEB_DIRS.items():
|
for name, dir in nodes.EXTENSION_WEB_DIRS.items():
|
||||||
self.app.add_routes([web.static('/extensions/' + name, dir)])
|
self.app.add_routes([web.static('/extensions/' + name, dir)])
|
||||||
|
|
||||||
workflow_templates_path = FrontendManager.templates_path()
|
installed_templates_version = FrontendManager.get_installed_templates_version()
|
||||||
if workflow_templates_path:
|
use_legacy_templates = True
|
||||||
self.app.add_routes([
|
if installed_templates_version:
|
||||||
web.static('/templates', workflow_templates_path)
|
try:
|
||||||
])
|
use_legacy_templates = (
|
||||||
|
parse_version(installed_templates_version)
|
||||||
|
< parse_version("0.3.0")
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logging.warning(
|
||||||
|
"Unable to parse templates version '%s': %s",
|
||||||
|
installed_templates_version,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
|
if use_legacy_templates:
|
||||||
|
workflow_templates_path = FrontendManager.legacy_templates_path()
|
||||||
|
if workflow_templates_path:
|
||||||
|
self.app.add_routes([
|
||||||
|
web.static('/templates', workflow_templates_path)
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
handler = FrontendManager.template_asset_handler()
|
||||||
|
if handler:
|
||||||
|
self.app.router.add_get("/templates/{path:.*}", handler)
|
||||||
|
|
||||||
# Serve embedded documentation from the package
|
# Serve embedded documentation from the package
|
||||||
embedded_docs_path = FrontendManager.embedded_docs_path()
|
embedded_docs_path = FrontendManager.embedded_docs_path()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user