From 83ec1998a907b73259cfc545242bc1d57fa7133b Mon Sep 17 00:00:00 2001 From: 171696257 <1761696257@qq.com> Date: Mon, 31 Mar 2025 12:58:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E8=8A=82=E7=82=B9?= =?UTF-8?q?=EF=BC=8C=E5=9B=BE=E5=83=8F=E7=BB=9F=E4=B8=80=E5=B0=BA=E5=AF=B8?= =?UTF-8?q?=E3=80=81=E5=A2=9E=E5=BC=BA=E5=8A=9F=E8=83=BD=E5=92=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +- __init__.py | 21 +- advanced_fusion.py => node/advanced_fusion.py | 0 color_generator.py => node/color_generator.py | 0 .../dimension_calculator.py | 0 node/image_resize.py | 232 +++++++++++++ image_to_video.py => node/image_to_video.py | 0 model_optimizer.py => node/model_optimizer.py | 0 .../sampling_optimizer.py | 316 +++++++++--------- simple_latent.py => node/simple_latent.py | 0 wan_latent.py => node/wan_latent.py | 192 +++++------ 11 files changed, 519 insertions(+), 265 deletions(-) rename advanced_fusion.py => node/advanced_fusion.py (100%) rename color_generator.py => node/color_generator.py (100%) rename dimension_calculator.py => node/dimension_calculator.py (100%) create mode 100644 node/image_resize.py rename image_to_video.py => node/image_to_video.py (100%) rename model_optimizer.py => node/model_optimizer.py (100%) rename sampling_optimizer.py => node/sampling_optimizer.py (97%) rename simple_latent.py => node/simple_latent.py (100%) rename wan_latent.py => node/wan_latent.py (95%) diff --git a/README.md b/README.md index 48eb1a0..7816a92 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,21 @@ Wan2.1的定制优化Latent节点 +
+ DD 图像统一尺寸 + +多功能图像和视频尺寸统一处理器: +- 支持动态调整输入和输出端口数量(无上限) +- 5种专业缩放算法(邻近-精确、双线性插值、区域、双三次插值、lanczos) +- 4种智能尺寸适配模式(自适应、拉伸、裁剪、填充) +- 支持批量处理多个图像或视频源 +- 支持图像和视频混合输入 +- 完全保留图像质量和透明通道 + +![统一尺寸示例](https://github.com/user-attachments/assets/统一尺寸示例.png) + +
+ ## 安装 1. 将文件复制到 ComfyUI 的 custom_nodes 目录 @@ -92,9 +107,13 @@ Wan2.1的定制优化Latent节点 3. 重启 ComfyUI ## 版本历史 +- v1.6.0(2025-03-05) + - 添加图像统一尺寸节点 + - 支持多端口动态调整 + - 支持多种缩放算法和尺寸适配模式 - v1.5.0(2025-03-02) - 添加空Latent视频(Wan2.1) - -采用Wan2.1的官方标准化Latent使结果获得更好的质量和遵从度。 + - 采用Wan2.1的官方标准化Latent使结果获得更好的质量和遵从度。 - v1.4.0(2025-03-01) - 添加采样优化器节点 - 实现首次采样延迟消除 @@ -133,7 +152,7 @@ Wan2.1的定制优化Latent节点 ## 反馈与建议 -这是一个持续成长的节点集合,主要用于分享实用的 ComfyUI 扩展工具。欢迎通过 Issues 提供建议和反馈。如果您发现任何问题或有任何改进建议,请告诉[...] +这是一个持续成长的节点集合,主要用于分享实用的 ComfyUI 扩展工具。欢迎通过 Issues 提供建议和反馈。如果您发现任何问题或有任何改进建议,请告诉我们。 ## 许可证 diff --git a/__init__.py b/__init__.py index dc7114e..5487b00 100644 --- a/__init__.py +++ b/__init__.py @@ -1,11 +1,12 @@ -from .color_generator import NODE_CLASS_MAPPINGS as COLOR_NODES -from .dimension_calculator import NODE_CLASS_MAPPINGS as DIMENSION_NODES -from .image_to_video import NODE_CLASS_MAPPINGS as VIDEO_NODES -from .advanced_fusion import NODE_CLASS_MAPPINGS as FUSION_NODES -from .simple_latent import NODE_CLASS_MAPPINGS as LATENT_NODES -from .model_optimizer import NODE_CLASS_MAPPINGS as OPTIMIZER_NODES -from .sampling_optimizer import NODE_CLASS_MAPPINGS as SAMPLING_NODES -from .wan_latent import NODE_CLASS_MAPPINGS as WAN_NODES +from .node.color_generator import NODE_CLASS_MAPPINGS as COLOR_NODES +from .node.dimension_calculator import NODE_CLASS_MAPPINGS as DIMENSION_NODES +from .node.image_to_video import NODE_CLASS_MAPPINGS as VIDEO_NODES +from .node.advanced_fusion import NODE_CLASS_MAPPINGS as FUSION_NODES +from .node.simple_latent import NODE_CLASS_MAPPINGS as LATENT_NODES +from .node.model_optimizer import NODE_CLASS_MAPPINGS as OPTIMIZER_NODES +from .node.sampling_optimizer import NODE_CLASS_MAPPINGS as SAMPLING_NODES +from .node.wan_latent import NODE_CLASS_MAPPINGS as WAN_NODES +from .node.image_resize import NODE_CLASS_MAPPINGS as RESIZE_NODES # 节点类映射 NODE_CLASS_MAPPINGS = { @@ -17,6 +18,7 @@ NODE_CLASS_MAPPINGS = { **OPTIMIZER_NODES, **SAMPLING_NODES, **WAN_NODES, + **RESIZE_NODES, } # 节点显示名称映射 @@ -28,7 +30,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "DD-SimpleLatent": "DD 极简Latent", "DD-ModelOptimizer": "DD 模型优化加载", "DD-SamplingOptimizer": "DD 采样优化器", - "DDEmptyWan21LatentVideo": "DD空Latent视频(Wan2.1)" + "DDEmptyWan21LatentVideo": "DD空Latent视频(Wan2.1)", + "DD-ImageUniformSize": "DD 图像统一尺寸" } # 导出模块 diff --git a/advanced_fusion.py b/node/advanced_fusion.py similarity index 100% rename from advanced_fusion.py rename to node/advanced_fusion.py diff --git a/color_generator.py b/node/color_generator.py similarity index 100% rename from color_generator.py rename to node/color_generator.py diff --git a/dimension_calculator.py b/node/dimension_calculator.py similarity index 100% rename from dimension_calculator.py rename to node/dimension_calculator.py diff --git a/node/image_resize.py b/node/image_resize.py new file mode 100644 index 0000000..f54a31a --- /dev/null +++ b/node/image_resize.py @@ -0,0 +1,232 @@ +import torch +import numpy as np +import cv2 +import comfy.utils +from typing import List, Dict, Any, Tuple + +class DDImageUniformSize: + """ + DD 图像统一尺寸 - 将输入的图像或视频统一调整为指定分辨率 + 支持多输入端口,多种缩放方法和尺寸适配策略 + 只有接入内容的输入端口才会生成对应的输出 + """ + + @classmethod + def INPUT_TYPES(cls): + # 基本配置 + inputs = { + "required": { + "缩放方法": (["邻近-精确", "双线性插值", "区域", "双三次插值", "lanczos"], {"default": "双线性插值"}), + "宽度": ("INT", {"default": 512, "min": 8, "max": 8192, "step": 8}), + "高度": ("INT", {"default": 512, "min": 8, "max": 8192, "step": 8}), + "尺寸适配": (["自适应", "拉伸", "裁剪", "填充"], {"default": "自适应"}), + }, + "optional": { + # 四个固定的可选输入端口 + "图片A": ("IMAGE",), + "图片B": ("IMAGE",), + "图片C": ("IMAGE",), + "图片D": ("IMAGE",), + } + } + return inputs + + # 默认输出端口设置 - 所有可能的输出 + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE") + RETURN_NAMES = ("图片A", "图片B", "图片C", "图片D") + FUNCTION = "resize_images" + CATEGORY = "🍺DD系列节点" + + # 常量:插值方法映射 + INTERPOLATION_MAP = { + "邻近-精确": cv2.INTER_NEAREST_EXACT, + "双线性插值": cv2.INTER_LINEAR, + "区域": cv2.INTER_AREA, + "双三次插值": cv2.INTER_CUBIC, + "lanczos": cv2.INTER_LANCZOS4 + } + + def _resize_batch(self, image_batch, target_size, interpolation_mode, size_adapt): + """调整一批图像的大小""" + if image_batch is None: + return None + + # 确保输入为张量 + if not isinstance(image_batch, torch.Tensor): + return None + + # 获取图像尺寸 + if len(image_batch.shape) == 3: # 单张图片 [H, W, C] + image_batch = image_batch.unsqueeze(0) # [1, H, W, C] + + batch_size, height, width, channels = image_batch.shape + target_height, target_width = target_size + result = None + + # 获取插值方法 + interpolation = self.INTERPOLATION_MAP.get(interpolation_mode, cv2.INTER_LINEAR) + + # 根据尺寸适配方法调整图像 + if size_adapt == "拉伸": + # 直接调整到目标尺寸 + result = self._batch_resize(image_batch, target_size, interpolation) + + elif size_adapt == "自适应": + # 保持宽高比缩放 + result = self._batch_adaptive_resize(image_batch, target_size, interpolation) + + elif size_adapt == "裁剪": + # 保持宽高比缩放后居中裁剪 + result = self._batch_center_crop(image_batch, target_size, interpolation) + + elif size_adapt == "填充": + # 保持宽高比缩放后填充 + result = self._batch_pad(image_batch, target_size, interpolation) + + else: + # 默认使用自适应 + result = self._batch_adaptive_resize(image_batch, target_size, interpolation) + + return result + + def _batch_resize(self, batch, target_size, interpolation): + """批量调整尺寸 - 直接拉伸""" + # 使用PyTorch内置的resize函数 + target_height, target_width = target_size + # 将图像从BHWC转换为BCHW + batch_bchw = batch.permute(0, 3, 1, 2) + + resized = torch.nn.functional.interpolate( + batch_bchw, + size=(target_height, target_width), + mode=self._get_torch_mode(interpolation), + align_corners=False if self._get_torch_mode(interpolation) != 'nearest' else None + ) + + # 转回BHWC + return resized.permute(0, 2, 3, 1) + + def _get_torch_mode(self, cv2_interpolation): + """将OpenCV插值模式转换为PyTorch模式""" + if cv2_interpolation in [cv2.INTER_NEAREST, cv2.INTER_NEAREST_EXACT]: + return 'nearest' + elif cv2_interpolation == cv2.INTER_LINEAR: + return 'bilinear' + elif cv2_interpolation == cv2.INTER_CUBIC: + return 'bicubic' + elif cv2_interpolation == cv2.INTER_AREA: + return 'area' + else: + return 'bilinear' # 默认返回双线性 + + def _batch_adaptive_resize(self, batch, target_size, interpolation): + """批量自适应调整尺寸 - 保持宽高比""" + batch_size, height, width, channels = batch.shape + target_height, target_width = target_size + + # 计算缩放比例 + ratio = min(target_width / width, target_height / height) + new_width = int(width * ratio) + new_height = int(height * ratio) + + # 先调整大小 + batch_bchw = batch.permute(0, 3, 1, 2) + resized = torch.nn.functional.interpolate( + batch_bchw, + size=(new_height, new_width), + mode=self._get_torch_mode(interpolation), + align_corners=False if self._get_torch_mode(interpolation) != 'nearest' else None + ) + + # 创建目标大小的空张量 + result = torch.zeros(batch_size, channels, target_height, target_width, device=batch.device) + + # 计算偏移量 + y_offset = (target_height - new_height) // 2 + x_offset = (target_width - new_width) // 2 + + # 将调整后的图像放在中心 + result[:, :, y_offset:y_offset + new_height, x_offset:x_offset + new_width] = resized + + # 转回BHWC + return result.permute(0, 2, 3, 1) + + def _batch_center_crop(self, batch, target_size, interpolation): + """批量中心裁剪 - 先调整大小然后裁剪""" + batch_size, height, width, channels = batch.shape + target_height, target_width = target_size + + # 计算缩放比例 - 以较大的比例为准,确保裁剪 + ratio = max(target_width / width, target_height / height) + new_width = int(width * ratio) + new_height = int(height * ratio) + + # 调整大小 + batch_bchw = batch.permute(0, 3, 1, 2) + resized = torch.nn.functional.interpolate( + batch_bchw, + size=(new_height, new_width), + mode=self._get_torch_mode(interpolation), + align_corners=False if self._get_torch_mode(interpolation) != 'nearest' else None + ) + + # 计算裁剪区域 + y_start = (new_height - target_height) // 2 + x_start = (new_width - target_width) // 2 + + # 裁剪中心区域 + cropped = resized[:, :, y_start:y_start + target_height, x_start:x_start + target_width] + + # 转回BHWC + return cropped.permute(0, 2, 3, 1) + + def _batch_pad(self, batch, target_size, interpolation): + """批量填充 - 保持宽高比并填充""" + # 这与自适应调整相同,因为我们已经创建了全零背景并居中放置调整后的图像 + return self._batch_adaptive_resize(batch, target_size, interpolation) + + def resize_images(self, 缩放方法, 宽度, 高度, 尺寸适配, 图片A=None, 图片B=None, 图片C=None, 图片D=None): + """根据指定参数统一调整所有输入图像的大小""" + target_size = (高度, 宽度) # (H, W) + results = [] + + # 创建输入图像和名称的映射 + input_images = { + "图片A": 图片A, + "图片B": 图片B, + "图片C": 图片C, + "图片D": 图片D + } + + # 过滤出有内容的输入 + valid_inputs = {name: img for name, img in input_images.items() if img is not None} + + # 动态设置输出类型和名称 + if len(valid_inputs) > 0: + self.RETURN_TYPES = tuple(["IMAGE"] * len(valid_inputs)) + self.RETURN_NAMES = tuple(valid_inputs.keys()) + else: + # 如果没有有效输入,提供一个默认输出 + self.RETURN_TYPES = ("IMAGE",) + self.RETURN_NAMES = ("图片A",) + # 创建默认空图像 + empty_image = torch.zeros(1, 高度, 宽度, 3) + return (empty_image,) + + # 处理每个有效输入 + for img in valid_inputs.values(): + # 调整图像大小 + resized = self._resize_batch(img, target_size, 缩放方法, 尺寸适配) + results.append(resized) + + return tuple(results) + +# 节点类映射 +NODE_CLASS_MAPPINGS = { + "DD-ImageUniformSize": DDImageUniformSize +} + +# 节点显示名称映射 +NODE_DISPLAY_NAME_MAPPINGS = { + "DD-ImageUniformSize": "DD 图像统一尺寸" +} diff --git a/image_to_video.py b/node/image_to_video.py similarity index 100% rename from image_to_video.py rename to node/image_to_video.py diff --git a/model_optimizer.py b/node/model_optimizer.py similarity index 100% rename from model_optimizer.py rename to node/model_optimizer.py diff --git a/sampling_optimizer.py b/node/sampling_optimizer.py similarity index 97% rename from sampling_optimizer.py rename to node/sampling_optimizer.py index d35ea0a..f391311 100644 --- a/sampling_optimizer.py +++ b/node/sampling_optimizer.py @@ -1,158 +1,158 @@ -import torch -import comfy.model_management as model_management -import gc -import time - -class DDSamplingOptimizer: - """ - DD 采样速度优化器 - 极简版 - 优化模型首次采样速度,减少第一次采样延迟 - 通过预热模型和CLIP参数减少首次采样时的CUDA延迟 - """ - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "模型": ("MODEL",), - "CLIP模型": ("CLIP",) - } - } - - RETURN_TYPES = ("MODEL", "CLIP",) - RETURN_NAMES = ("优化模型", "优化CLIP",) - FUNCTION = "optimize_sampling" - CATEGORY = "🍺DD系列节点" - - def log_progress(self, message): - """输出进度信息""" - print(f"[采样优化器] {message}") - - def warmup_model(self, model): - """模型预热核心方法 - 基于ComfyUI官方实现的优化方案""" - try: - # 对ModelPatcher类型的特殊处理 - if hasattr(model, "model") and model.model is not None: - self.log_progress("通过model属性访问底层模型") - - # 访问内部模型的模块参数 - if hasattr(model.model, "modules"): - self.log_progress("访问内部模型的modules") - for module in model.model.modules(): - if hasattr(module, "_parameters"): - for param in module._parameters.values(): - if param is not None: - # 只是访问参数以触发CUDA预热 - _ = param.device - break - - # 尝试访问model属性的参数 - self.log_progress("尝试通过model属性访问参数") - try: - for param in model.model.parameters(): - _ = param.device - break - self.log_progress("model参数访问成功") - except Exception: - pass - - # 如果上述方法失败,尝试直接访问模型 - elif hasattr(model, "modules"): - self.log_progress("直接访问模型modules") - for module in model.modules(): - if hasattr(module, "_parameters"): - for param in module._parameters.values(): - if param is not None: - _ = param.device - break - - self.log_progress("模型基本优化完成") - return True - except Exception as e: - self.log_progress(f"模型预热失败: {str(e)}") - return False - - def warmup_clip(self, clip_model): - """CLIP模型预热核心方法 - 保留最可靠的方法""" - self.log_progress("CLIP预热开始") - try: - with torch.no_grad(): - # 尝试最常见的CLIP方法 - if hasattr(clip_model, "encode_from_tokens") and hasattr(clip_model, "tokenize"): - try: - empty_tokens = clip_model.tokenize("") - clip_model.encode_from_tokens(empty_tokens, return_pooled=True) - return - except Exception: - pass - - # 备用方法 - if hasattr(clip_model, "encode"): - try: - clip_model.encode("a photo") - return - except Exception: - pass - - # 最基本的方法 - 访问参数 - for param in clip_model.parameters(): - _ = param.device - break - - self.log_progress("CLIP预热完成") - except Exception as e: - self.log_progress(f"CLIP预热失败: {str(e)}") - - def optimize_sampling(self, 模型, CLIP模型): - """ - 优化模型首次采样速度 - 简化版 - - Args: - 模型: 需要优化的扩散模型 - CLIP模型: CLIP文本编码器模型 - - Returns: - 优化后的模型和CLIP - """ - try: - start_time = time.time() - self.log_progress("开始优化模型采样速度") - - # 清理CUDA缓存 - if torch.cuda.is_available(): - torch.cuda.empty_cache() - gc.collect() - - # 执行模型预热 - self.log_progress("开始模型预热...") - self.warmup_model(模型) - - # CLIP模型预热 - self.log_progress("开始CLIP模型预热...") - self.warmup_clip(CLIP模型) - - # 清理缓存 - if torch.cuda.is_available(): - torch.cuda.empty_cache() - gc.collect() - - # 统计时间 - elapsed_time = time.time() - start_time - self.log_progress(f"采样优化完成,耗时 {elapsed_time:.2f} 秒") - self.log_progress("后续采样过程应该不会出现明显的首次延迟") - - return (模型, CLIP模型) - - except Exception as e: - self.log_progress(f"优化过程发生错误: {str(e)}") - return (模型, CLIP模型) - -# 节点映射 -NODE_CLASS_MAPPINGS = { - "DD-SamplingOptimizer": DDSamplingOptimizer -} - -# 节点显示名称映射 -NODE_DISPLAY_NAME_MAPPINGS = { - "DD-SamplingOptimizer": "DD 采样优化器" -} +import torch +import comfy.model_management as model_management +import gc +import time + +class DDSamplingOptimizer: + """ + DD 采样速度优化器 - 极简版 + 优化模型首次采样速度,减少第一次采样延迟 + 通过预热模型和CLIP参数减少首次采样时的CUDA延迟 + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "模型": ("MODEL",), + "CLIP模型": ("CLIP",) + } + } + + RETURN_TYPES = ("MODEL", "CLIP",) + RETURN_NAMES = ("优化模型", "优化CLIP",) + FUNCTION = "optimize_sampling" + CATEGORY = "🍺DD系列节点" + + def log_progress(self, message): + """输出进度信息""" + print(f"[采样优化器] {message}") + + def warmup_model(self, model): + """模型预热核心方法 - 基于ComfyUI官方实现的优化方案""" + try: + # 对ModelPatcher类型的特殊处理 + if hasattr(model, "model") and model.model is not None: + self.log_progress("通过model属性访问底层模型") + + # 访问内部模型的模块参数 + if hasattr(model.model, "modules"): + self.log_progress("访问内部模型的modules") + for module in model.model.modules(): + if hasattr(module, "_parameters"): + for param in module._parameters.values(): + if param is not None: + # 只是访问参数以触发CUDA预热 + _ = param.device + break + + # 尝试访问model属性的参数 + self.log_progress("尝试通过model属性访问参数") + try: + for param in model.model.parameters(): + _ = param.device + break + self.log_progress("model参数访问成功") + except Exception: + pass + + # 如果上述方法失败,尝试直接访问模型 + elif hasattr(model, "modules"): + self.log_progress("直接访问模型modules") + for module in model.modules(): + if hasattr(module, "_parameters"): + for param in module._parameters.values(): + if param is not None: + _ = param.device + break + + self.log_progress("模型基本优化完成") + return True + except Exception as e: + self.log_progress(f"模型预热失败: {str(e)}") + return False + + def warmup_clip(self, clip_model): + """CLIP模型预热核心方法 - 保留最可靠的方法""" + self.log_progress("CLIP预热开始") + try: + with torch.no_grad(): + # 尝试最常见的CLIP方法 + if hasattr(clip_model, "encode_from_tokens") and hasattr(clip_model, "tokenize"): + try: + empty_tokens = clip_model.tokenize("") + clip_model.encode_from_tokens(empty_tokens, return_pooled=True) + return + except Exception: + pass + + # 备用方法 + if hasattr(clip_model, "encode"): + try: + clip_model.encode("a photo") + return + except Exception: + pass + + # 最基本的方法 - 访问参数 + for param in clip_model.parameters(): + _ = param.device + break + + self.log_progress("CLIP预热完成") + except Exception as e: + self.log_progress(f"CLIP预热失败: {str(e)}") + + def optimize_sampling(self, 模型, CLIP模型): + """ + 优化模型首次采样速度 - 简化版 + + Args: + 模型: 需要优化的扩散模型 + CLIP模型: CLIP文本编码器模型 + + Returns: + 优化后的模型和CLIP + """ + try: + start_time = time.time() + self.log_progress("开始优化模型采样速度") + + # 清理CUDA缓存 + if torch.cuda.is_available(): + torch.cuda.empty_cache() + gc.collect() + + # 执行模型预热 + self.log_progress("开始模型预热...") + self.warmup_model(模型) + + # CLIP模型预热 + self.log_progress("开始CLIP模型预热...") + self.warmup_clip(CLIP模型) + + # 清理缓存 + if torch.cuda.is_available(): + torch.cuda.empty_cache() + gc.collect() + + # 统计时间 + elapsed_time = time.time() - start_time + self.log_progress(f"采样优化完成,耗时 {elapsed_time:.2f} 秒") + self.log_progress("后续采样过程应该不会出现明显的首次延迟") + + return (模型, CLIP模型) + + except Exception as e: + self.log_progress(f"优化过程发生错误: {str(e)}") + return (模型, CLIP模型) + +# 节点映射 +NODE_CLASS_MAPPINGS = { + "DD-SamplingOptimizer": DDSamplingOptimizer +} + +# 节点显示名称映射 +NODE_DISPLAY_NAME_MAPPINGS = { + "DD-SamplingOptimizer": "DD 采样优化器" +} diff --git a/simple_latent.py b/node/simple_latent.py similarity index 100% rename from simple_latent.py rename to node/simple_latent.py diff --git a/wan_latent.py b/node/wan_latent.py similarity index 95% rename from wan_latent.py rename to node/wan_latent.py index b623835..023eb16 100644 --- a/wan_latent.py +++ b/node/wan_latent.py @@ -1,96 +1,96 @@ -import torch -import comfy.model_management -from comfy.latent_formats import Wan21 -import nodes -import re - -class DDEmptyWan21LatentVideo: - """为Wan2.1模型创建空Latent视频,支持推荐分辨率和标准化处理""" - - @classmethod - def INPUT_TYPES(cls): - # Wan2.1支持的推荐分辨率列表,使用Unicode符号增强视觉效果 - wan_resolutions = [ - "🖥️ 横屏 832×480 (26:15)", - "🖥️ 横屏 1088×832 (4:3)", - "🖥️ 横屏 1280×704 (16:9)", - "📱 竖屏 480×832 (15:26)", - "📱 竖屏 832×1088 (3:4)", - "📱 竖屏 704×1280 (9:16)", - "⬛ 方屏 624×624 (1:1)", - "⬛ 方屏 960×960 (1:1)", - "⬛ 方屏 1280×1280 (1:1)" - ] - - return { - "required": { - "使用推荐分辨率": ("BOOLEAN", {"default": True}), - "应用标准化": ("BOOLEAN", {"default": True}), - "推荐分辨率": (wan_resolutions, {"default": "🖥️ 横屏 832×480 (26:15)"}), - "宽度": ("INT", {"default": 832, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}), - "高度": ("INT", {"default": 480, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}), - "帧数": ("INT", {"default": 81, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 4}), - "批次大小": ("INT", {"default": 1, "min": 1, "max": 4096}), - } - } - - RETURN_TYPES = ("LATENT",) - RETURN_NAMES = ("潜空间",) - FUNCTION = "generate_latent" - CATEGORY = "🍺DD系列节点" - - def generate_latent(self, 使用推荐分辨率, 应用标准化, 推荐分辨率, 宽度, 高度, 帧数, 批次大小=1): - """生成Wan2.1空Latent视频""" - try: - # 如果使用推荐分辨率,则解析推荐的宽高 - if 使用推荐分辨率: - # 使用正则表达式提取分辨率,适应新格式 - resolution_match = re.search(r'(\d+)×(\d+)', 推荐分辨率) - if resolution_match: - 宽度 = int(resolution_match.group(1)) - 高度 = int(resolution_match.group(2)) - - # 确保宽高是8的倍数 - 宽度 = (宽度 // 8) * 8 - 高度 = (高度 // 8) * 8 - - # 计算正确的时间维度 - time_dim = ((帧数 - 1) // 4) + 1 - - # 创建空的latent - latent = torch.zeros( - [批次大小, 16, time_dim, 高度 // 8, 宽度 // 8], - device=comfy.model_management.intermediate_device() - ) - - # 如果需要归一化,应用Wan21格式的均值和标准差 - if 应用标准化: - # 初始化Wan21格式以获取均值和标准差 - wan_format = Wan21() - - # 获取正确设备和dtype的均值和标准差 - latents_mean = wan_format.latents_mean.to(latent.device, latent.dtype) - latents_std = wan_format.latents_std.to(latent.device, latent.dtype) - - # 应用归一化: (latent - mean) / std - latent = (latent - latents_mean) * wan_format.scale_factor / latents_std - - return ({"samples": latent},) - - except Exception as e: - print(f"[DD空Latent视频(Wan2.1)] 错误: {str(e)}") - # 在出错的情况下仍然返回一个基本的latent - basic_latent = torch.zeros( - [批次大小, 16, ((帧数 - 1) // 4) + 1, 高度 // 8, 宽度 // 8], - device=comfy.model_management.intermediate_device() - ) - return ({"samples": basic_latent},) - -# 节点注册 -NODE_CLASS_MAPPINGS = { - "DDEmptyWan21LatentVideo": DDEmptyWan21LatentVideo, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "DDEmptyWan21LatentVideo": "DD空Latent视频(Wan2.1)", -} +import torch +import comfy.model_management +from comfy.latent_formats import Wan21 +import nodes +import re + +class DDEmptyWan21LatentVideo: + """为Wan2.1模型创建空Latent视频,支持推荐分辨率和标准化处理""" + + @classmethod + def INPUT_TYPES(cls): + # Wan2.1支持的推荐分辨率列表,使用Unicode符号增强视觉效果 + wan_resolutions = [ + "🖥️ 横屏 832×480 (26:15)", + "🖥️ 横屏 1088×832 (4:3)", + "🖥️ 横屏 1280×720 (16:9)", + "📱 竖屏 480×832 (15:26)", + "📱 竖屏 832×1088 (3:4)", + "📱 竖屏 720×1280 (9:16)", + "⬛ 方屏 624×624 (1:1)", + "⬛ 方屏 960×960 (1:1)", + "⬛ 方屏 1280×1280 (1:1)" + ] + + return { + "required": { + "使用推荐分辨率": ("BOOLEAN", {"default": True}), + "应用标准化": ("BOOLEAN", {"default": True}), + "推荐分辨率": (wan_resolutions, {"default": "🖥️ 横屏 832×480 (26:15)"}), + "宽度": ("INT", {"default": 832, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}), + "高度": ("INT", {"default": 480, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}), + "帧数": ("INT", {"default": 81, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 4}), + "批次大小": ("INT", {"default": 1, "min": 1, "max": 4096}), + } + } + + RETURN_TYPES = ("LATENT",) + RETURN_NAMES = ("潜空间",) + FUNCTION = "generate_latent" + CATEGORY = "🍺DD系列节点" + + def generate_latent(self, 使用推荐分辨率, 应用标准化, 推荐分辨率, 宽度, 高度, 帧数, 批次大小=1): + """生成Wan2.1空Latent视频""" + try: + # 如果使用推荐分辨率,则解析推荐的宽高 + if 使用推荐分辨率: + # 使用正则表达式提取分辨率,适应新格式 + resolution_match = re.search(r'(\d+)×(\d+)', 推荐分辨率) + if resolution_match: + 宽度 = int(resolution_match.group(1)) + 高度 = int(resolution_match.group(2)) + + # 确保宽高是8的倍数 + 宽度 = (宽度 // 8) * 8 + 高度 = (高度 // 8) * 8 + + # 计算正确的时间维度 + time_dim = ((帧数 - 1) // 4) + 1 + + # 创建空的latent + latent = torch.zeros( + [批次大小, 16, time_dim, 高度 // 8, 宽度 // 8], + device=comfy.model_management.intermediate_device() + ) + + # 如果需要归一化,应用Wan21格式的均值和标准差 + if 应用标准化: + # 初始化Wan21格式以获取均值和标准差 + wan_format = Wan21() + + # 获取正确设备和dtype的均值和标准差 + latents_mean = wan_format.latents_mean.to(latent.device, latent.dtype) + latents_std = wan_format.latents_std.to(latent.device, latent.dtype) + + # 应用归一化: (latent - mean) / std + latent = (latent - latents_mean) * wan_format.scale_factor / latents_std + + return ({"samples": latent},) + + except Exception as e: + print(f"[DD空Latent视频(Wan2.1)] 错误: {str(e)}") + # 在出错的情况下仍然返回一个基本的latent + basic_latent = torch.zeros( + [批次大小, 16, ((帧数 - 1) // 4) + 1, 高度 // 8, 宽度 // 8], + device=comfy.model_management.intermediate_device() + ) + return ({"samples": basic_latent},) + +# 节点注册 +NODE_CLASS_MAPPINGS = { + "DDEmptyWan21LatentVideo": DDEmptyWan21LatentVideo, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DDEmptyWan21LatentVideo": "DD空Latent视频(Wan2.1)", +}