Merge branch 'Comfy-Org:master' into master

This commit is contained in:
azazeal04 2026-05-29 13:15:34 +02:00 committed by GitHub
commit 3868ba4828
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 99 additions and 23 deletions

View File

@ -0,0 +1,24 @@
name: Detect Unreviewed Merge
# SOC 2 compliance — reusable workflow lives in Comfy-Org/github-workflows,
# tracking issues are filed in Comfy-Org/unreviewed-merges.
on:
push:
branches: [master]
concurrency:
group: detect-unreviewed-merge-${{ github.sha }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: read
jobs:
detect:
uses: Comfy-Org/github-workflows/.github/workflows/detect-unreviewed-merge.yml@4d9cb6b87f953bb7cd69954280e1465fb9bd2040 # v1
with:
approval-mode: latest-per-reviewer
secrets:
UNREVIEWED_MERGES_TOKEN: ${{ secrets.UNREVIEWED_MERGES_TOKEN }}

View File

@ -1,5 +1,20 @@
import logging
import torch
_CK_STOCHASTIC_ROUNDING_AVAILABLE = False
try:
import comfy_kitchen as ck
_ck_stochastic_rounding_fp8 = ck.stochastic_rounding_fp8
_CK_STOCHASTIC_ROUNDING_AVAILABLE = True
except (AttributeError, ImportError):
logging.warning("comfy_kitchen does not support stochastic FP8 rounding, please update comfy_kitchen.")
if not _CK_STOCHASTIC_ROUNDING_AVAILABLE:
def _ck_stochastic_rounding_fp8(value, rng, dtype):
raise NotImplementedError("comfy_kitchen does not support stochastic FP8 rounding")
def calc_mantissa(abs_x, exponent, normal_mask, MANTISSA_BITS, EXPONENT_BIAS, generator=None):
mantissa_scaled = torch.where(
normal_mask,
@ -57,6 +72,10 @@ def stochastic_rounding(value, dtype, seed=0):
if dtype == torch.float8_e4m3fn or dtype == torch.float8_e5m2:
generator = torch.Generator(device=value.device)
generator.manual_seed(seed)
if _CK_STOCHASTIC_ROUNDING_AVAILABLE:
rng = torch.randint(0, 256, value.size(), dtype=torch.uint8, layout=value.layout, device=value.device, generator=generator)
return _ck_stochastic_rounding_fp8(value, rng, dtype)
output = torch.empty_like(value, dtype=dtype)
num_slices = max(1, (value.numel() / (4096 * 4096)))
slice_size = max(1, round(value.shape[0] / num_slices))

View File

@ -762,21 +762,32 @@ class Accumulation(ComfyTypeIO):
@comfytype(io_type="LOAD3D_CAMERA")
class Load3DCamera(ComfyTypeIO):
class CameraInfo(TypedDict):
position: dict[str, float | int]
target: dict[str, float | int]
zoom: int
cameraType: str
quaternion: NotRequired[dict[str, float | int]]
rotation: NotRequired[dict[str, float | int | str]]
fov: NotRequired[float | int]
aspect: NotRequired[float | int]
near: NotRequired[float | int]
far: NotRequired[float | int]
frustum: NotRequired[dict[str, float | int]]
# Coordinate system: right-handed, Y-up, camera looks down -Z
position: dict[str, float | int] # scene units
target: dict[str, float | int] # scene units; OrbitControls focus point
zoom: float | int # dimensionless, 1 = 100%
cameraType: str # 'perspective' | 'orthographic'
quaternion: NotRequired[dict[str, float | int]] # normalized, dimensionless; camera world rotation
fov: NotRequired[float | int] # degrees, vertical FOV (perspective only)
aspect: NotRequired[float | int] # width / height (perspective only)
near: NotRequired[float | int] # scene units
far: NotRequired[float | int] # scene units
frustum: NotRequired[dict[str, float | int]] # orthographic only: {left, right, top, bottom} in scene units
Type = CameraInfo
@comfytype(io_type="LOAD3D_MODEL_INFO")
class Load3DModelInfo(ComfyTypeIO):
class Model3DTransform(TypedDict):
# Coordinate system: right-handed, Y-up, world space
position: dict[str, float | int] # scene units
quaternion: dict[str, float | int] # normalized, dimensionless; world rotation
scale: dict[str, float | int] # dimensionless multiplier
Type = list[Model3DTransform]
@comfytype(io_type="LOAD_3D")
class Load3D(ComfyTypeIO):
"""3D models are stored as a dictionary."""
@ -786,6 +797,7 @@ class Load3D(ComfyTypeIO):
normal: str
camera_info: Load3DCamera.CameraInfo
recording: NotRequired[str]
model_3d_info: NotRequired[list[Load3DModelInfo.Model3DTransform]]
Type = Model3DDict
@ -2298,6 +2310,7 @@ __all__ = [
"FlowControl",
"Accumulation",
"Load3DCamera",
"Load3DModelInfo",
"Load3D",
"Load3DAnimation",
"Photomaker",

View File

@ -206,7 +206,7 @@ class BeebleSwitchXVideoEdit(IO.ComfyNode):
return IO.Schema(
node_id="BeebleSwitchXVideoEdit",
display_name="Beeble SwitchX Video Edit",
category="api node/video/Beeble",
category="video/partner/Beeble",
description=(
"Edit a video with Beeble SwitchX. Switches anything in the scene (background, "
"lighting, costume) while preserving the original subject's pixels and motion. "
@ -302,7 +302,7 @@ class BeebleSwitchXImageEdit(IO.ComfyNode):
return IO.Schema(
node_id="BeebleSwitchXImageEdit",
display_name="Beeble SwitchX Image Edit",
category="api node/image/Beeble",
category="image/partner/Beeble",
description=(
"Edit a single image with Beeble SwitchX. Switches anything in the scene "
"(background, lighting, costume) while preserving the original subject's pixels. "

View File

@ -157,7 +157,7 @@ class LoadImageTextDataSetFromFolderNode(io.ComfyNode):
return io.NodeOutput(output_tensor, captions)
def save_images_to_folder(image_list, output_dir, prefix="image"):
def save_images_to_folder(image_list, output_dir, prefix="image", overwrite=True):
"""Utility function to save a list of image tensors to disk.
Args:
@ -197,7 +197,11 @@ def save_images_to_folder(image_list, output_dir, prefix="image"):
raise ValueError(f"Expected torch.Tensor, got {type(img_tensor)}")
# Save image
filename = f"{prefix}_{idx:05d}.png"
if overwrite:
filename = f"{prefix}_{idx:05d}.png"
else:
_, _, counter, _, resolved_prefix = folder_paths.get_save_image_path(prefix, output_dir)
filename = f"{resolved_prefix}_{counter:05}_{idx:05d}.png"
filepath = os.path.join(output_dir, filename)
img.save(filepath)
saved_files.append(filename)
@ -230,19 +234,26 @@ class SaveImageDataSetToFolderNode(io.ComfyNode):
tooltip="Prefix for saved image filenames.",
advanced=True,
),
io.Combo.Input(
"mode",
default="overwrite",
options=["overwrite", "increment"],
tooltip="Whether to overwrite existing files or increment filenames to avoid overwriting."
),
],
outputs=[],
is_deprecated=True, # This node is redundant and superseded by existing Save Image nodes where the target folder can be specified in the filename_prefix
)
@classmethod
def execute(cls, images, folder_name, filename_prefix):
def execute(cls, images, folder_name, filename_prefix, mode):
# Extract scalar values
folder_name = folder_name[0]
filename_prefix = filename_prefix[0]
mode = mode[0]
output_dir = os.path.join(folder_paths.get_output_directory(), folder_name)
saved_files = save_images_to_folder(images, output_dir, filename_prefix)
saved_files = save_images_to_folder(images, output_dir, filename_prefix, mode=='overwrite')
logging.info(f"Saved {len(saved_files)} images to {output_dir}.")
return io.NodeOutput()
@ -278,18 +289,25 @@ class SaveImageTextDataSetToFolderNode(io.ComfyNode):
tooltip="Prefix for saved image filenames.",
advanced=True,
),
io.Combo.Input(
"mode",
default="overwrite",
options=["overwrite", "increment"],
tooltip="Whether to overwrite existing files or increment filenames to avoid overwriting."
),
],
outputs=[],
)
@classmethod
def execute(cls, images, folder_name, filename_prefix, texts=None):
def execute(cls, images, folder_name, filename_prefix, mode, texts=None):
# Extract scalar values
folder_name = folder_name[0]
filename_prefix = filename_prefix[0]
mode = mode[0]
output_dir = os.path.join(folder_paths.get_output_directory(), folder_name)
saved_files = save_images_to_folder(images, output_dir, filename_prefix)
saved_files = save_images_to_folder(images, output_dir, filename_prefix, mode=='overwrite')
# Save captions
if texts:

View File

@ -47,6 +47,7 @@ class Load3D(IO.ComfyNode):
IO.Load3DCamera.Output(display_name="camera_info"),
IO.Video.Output(display_name="recording_video"),
IO.File3DAny.Output(display_name="model_3d"),
IO.Load3DModelInfo.Output(display_name="model_3d_info"),
],
)
@ -73,7 +74,8 @@ class Load3D(IO.ComfyNode):
if model_file and model_file != "none":
file_3d = Types.File3D(folder_paths.get_annotated_filepath(model_file))
mesh_path = model_file
return IO.NodeOutput(output_image, output_mask, mesh_path, normal_image, image['camera_info'], video, file_3d)
model_3d_info = image.get('model_3d_info', [])
return IO.NodeOutput(output_image, output_mask, mesh_path, normal_image, image['camera_info'], video, file_3d, model_3d_info)
process = execute # TODO: remove

View File

@ -1,5 +1,5 @@
comfyui-frontend-package==1.44.19
comfyui-workflow-templates==0.9.85
comfyui-workflow-templates==0.9.91
comfyui-embedded-docs==0.5.1
torch
torchsde
@ -21,8 +21,8 @@ psutil
alembic
SQLAlchemy>=2.0.0
filelock
av>=14.2.0
comfy-kitchen>=0.2.8
av>=16.0.0
comfy-kitchen==0.2.9
comfy-aimdo==0.4.5
requests
simpleeval>=1.0.0