v3.1.0 添加 Save Video by Image 节点,优化相关节点
This commit is contained in:
+5
-1
@@ -21,6 +21,10 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
**v3.1.0**
|
||||
- feat(io): 添加 `Save Video by Image` 节点,用于将图像序列编码保存为视频
|
||||
- refactor(color): 优化 `Match Brightness Contrast` 节点一致性选项
|
||||
|
||||
**v3.0.8**
|
||||
- refactor(image_crop): 优化 `Image Crop With BBox Mask` 节点
|
||||
- refactor(color): 优化 `Match Brightness Contrast` 节点
|
||||
@@ -493,8 +497,8 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes
|
||||
| Get File Count | 获取文件夹中指定类型(图像/视频)的文件数量,支持子文件夹递归 |
|
||||
| Load Image From Folder | 从文件夹加载指定索引的图像,支持批量处理和子文件夹 |
|
||||
| Load Video From Folder | 从文件夹加载指定索引的视频,返回 VIDEO 对象 |
|
||||
| Save Video by Image | 将图像序列合并并编码保存为视频,支持可选音频合成 |
|
||||
| Save Video | 保存视频到输出;支持空值输入,容器与编码器自动选择 |
|
||||
| Save Video RGBA | 将 images 保存为视频;支持 Alpha 预览与导出、可选音频、FPS 设置 |
|
||||
|
||||
### 🔊 音频节点
|
||||
| 节点名称 | 功能描述 |
|
||||
|
||||
@@ -21,6 +21,10 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes
|
||||
|
||||
## 📜 Changelog
|
||||
|
||||
**v3.1.0**
|
||||
- feat(io): Add `Save Video by Image` node for encoding IMAGE batches into video
|
||||
- refactor(color): Enhance `Match Brightness Contrast` sequence consistency options
|
||||
|
||||
**v3.0.8**
|
||||
- refactor(image_crop): Optimize `Image Crop With BBox Mask` node
|
||||
- refactor(color): Optimize `Match Brightness Contrast` node
|
||||
@@ -493,8 +497,8 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes
|
||||
| Get File Count | Get file count of specified type (image/video) in folder, supports recursive subfolder search |
|
||||
| Load Image From Folder | Load image at specified index from folder, supports batch processing and subfolders |
|
||||
| Load Video From Folder | Load video at specified index from folder, returns VIDEO object |
|
||||
| Save Video by Image | Encode IMAGE batch into video with optional audio muxing |
|
||||
| Save Video | Save VIDEO to output; supports optional input, auto container/codec selection |
|
||||
| Save Video RGBA | Save images to video; supports alpha preview/output, optional audio, configurable FPS |
|
||||
|
||||
### 🔊 Audio Nodes
|
||||
| Node Name | Description |
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72ad2326813c2ac80d7cb2d8d2eab6c476477ff42481599b89cf802717f0cef3
|
||||
size 98006
|
||||
oid sha256:ddeb1c5d300da986a4581874687164488759fb75ecf28e166ebf992c89f29329
|
||||
size 102142
|
||||
|
||||
@@ -22,7 +22,16 @@ class MatchBrightnessContrast(io.ComfyNode):
|
||||
io.Image.Input("source_image"),
|
||||
io.Image.Input("reference_image"),
|
||||
io.Float.Input("edge_amount", default=0.2, min=0.0, max=8192.0, step=0.01, display_mode=io.NumberDisplay.number),
|
||||
io.Combo.Input("consistency", options=["lock_first", "frame_match"], default="lock_first"),
|
||||
io.Combo.Input(
|
||||
"consistency",
|
||||
options=[
|
||||
"lock_first",
|
||||
"lock_mid",
|
||||
"lock_end",
|
||||
"frame_match",
|
||||
],
|
||||
default="lock_first",
|
||||
),
|
||||
io.Combo.Input("method", options=["standard", "histogram"], default="histogram"),
|
||||
],
|
||||
outputs=[
|
||||
@@ -50,11 +59,20 @@ class MatchBrightnessContrast(io.ComfyNode):
|
||||
|
||||
# 预计算第一帧的统计信息或映射表 (如果开启了时序一致性)
|
||||
locked_params = None
|
||||
if consistency == "lock_first" and src_batch > 0:
|
||||
# 使用第一帧源图像和对应的参考图像计算参数
|
||||
ref_idx = 0
|
||||
if consistency in {"lock_first", "lock_mid", "lock_end"} and src_batch > 0:
|
||||
# 使用选定帧的源图像和参考图像计算参数
|
||||
if consistency == "lock_mid":
|
||||
ref_idx = ref_batch // 2
|
||||
src_idx = src_batch // 2
|
||||
elif consistency == "lock_end":
|
||||
ref_idx = ref_batch - 1
|
||||
src_idx = src_batch - 1
|
||||
else:
|
||||
ref_idx = 0
|
||||
src_idx = 0
|
||||
|
||||
ref_img = reference_image[ref_idx % ref_batch]
|
||||
src_img = source_image[0]
|
||||
src_img = source_image[src_idx]
|
||||
|
||||
if method == "histogram":
|
||||
locked_params = cls._calculate_histogram_luts(src_img, ref_img, margin_src, margin_ref)
|
||||
@@ -230,4 +248,3 @@ class MatchBrightnessContrast(io.ComfyNode):
|
||||
else:
|
||||
stats = cls._calculate_standard_stats(src_img, ref_img, margin_src, margin_ref)
|
||||
return cls._apply_standard_stats(src_img, stats)
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ class ImageMainStitch(io.ComfyNode):
|
||||
io.String.Input("spacing_color", default="1.0"),
|
||||
io.String.Input("pad_color", default="1.0"),
|
||||
],
|
||||
outputs=[io.Image.Output(display_name="image")],
|
||||
outputs=[
|
||||
io.Image.Output(display_name="image"),
|
||||
io.Mask.Output(display_name="mask"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -71,7 +74,12 @@ class ImageMainStitch(io.ComfyNode):
|
||||
|
||||
if len(others) == 0:
|
||||
image = a.to(dtype=torch.float32).clamp(0.0, 1.0)
|
||||
return io.NodeOutput(image)
|
||||
mask = torch.ones(
|
||||
(image.shape[0], image.shape[1], image.shape[2]),
|
||||
dtype=torch.float32,
|
||||
device=image.device,
|
||||
)
|
||||
return io.NodeOutput(image, mask)
|
||||
|
||||
bs = a.shape[0]
|
||||
for t in others:
|
||||
@@ -83,6 +91,7 @@ class ImageMainStitch(io.ComfyNode):
|
||||
pad_rgb = cls._parse_pad_color(pad_color)
|
||||
|
||||
if direction in ("top", "bottom"):
|
||||
|
||||
def build_pair_h():
|
||||
cur = others[0]
|
||||
for t in others[1:]:
|
||||
@@ -96,8 +105,9 @@ class ImageMainStitch(io.ComfyNode):
|
||||
bs,
|
||||
)
|
||||
return cur
|
||||
|
||||
pair = await asyncio.to_thread(build_pair_h)
|
||||
out = await asyncio.to_thread(
|
||||
out, mask = await asyncio.to_thread(
|
||||
cls._stack_vertical,
|
||||
a,
|
||||
pair,
|
||||
@@ -109,6 +119,7 @@ class ImageMainStitch(io.ComfyNode):
|
||||
bs,
|
||||
)
|
||||
else:
|
||||
|
||||
def build_pair_v():
|
||||
cur = others[0]
|
||||
for t in others[1:]:
|
||||
@@ -122,8 +133,9 @@ class ImageMainStitch(io.ComfyNode):
|
||||
bs,
|
||||
)
|
||||
return cur
|
||||
|
||||
pair = await asyncio.to_thread(build_pair_v)
|
||||
out = await asyncio.to_thread(
|
||||
out, mask = await asyncio.to_thread(
|
||||
cls._stack_horizontal,
|
||||
a,
|
||||
pair,
|
||||
@@ -136,7 +148,7 @@ class ImageMainStitch(io.ComfyNode):
|
||||
)
|
||||
|
||||
image = torch.clamp(out, min=0.0, max=1.0).to(torch.float32)
|
||||
return io.NodeOutput(image)
|
||||
return io.NodeOutput(image, mask)
|
||||
|
||||
# --- 合并 2 与 3 ---
|
||||
@classmethod
|
||||
@@ -224,9 +236,21 @@ class ImageMainStitch(io.ComfyNode):
|
||||
# 间隔条宽度与最终统一宽度一致
|
||||
strip = cls._make_strip(spacing_width, w1 if match else unified_w, space_rgb, axis="h", dtype=img1.dtype, device=img1.device, batch_size=batch_size)
|
||||
|
||||
# mask
|
||||
b = img1.shape[0]
|
||||
mask_1 = torch.ones((b, h1, w1), dtype=torch.float32, device=img1.device)
|
||||
mask_p = torch.zeros((b, hp, wp), dtype=torch.float32, device=img1.device)
|
||||
mask_s = torch.zeros((b, strip.shape[1], strip.shape[2]), dtype=torch.float32, device=img1.device)
|
||||
|
||||
if direction == "bottom":
|
||||
return torch.cat([img1, strip, pair], dim=1)
|
||||
return torch.cat([pair, strip, img1], dim=1)
|
||||
return (
|
||||
torch.cat([img1, strip, pair], dim=1),
|
||||
torch.cat([mask_1, mask_s, mask_p], dim=1),
|
||||
)
|
||||
return (
|
||||
torch.cat([pair, strip, img1], dim=1),
|
||||
torch.cat([mask_p, mask_s, mask_1], dim=1),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _stack_horizontal(
|
||||
@@ -258,9 +282,21 @@ class ImageMainStitch(io.ComfyNode):
|
||||
# 间隔条高度与最终统一高度一致
|
||||
strip = cls._make_strip(h1 if match else unified_h, spacing_width, space_rgb, axis="v", dtype=img1.dtype, device=img1.device, batch_size=batch_size)
|
||||
|
||||
# mask
|
||||
b = img1.shape[0]
|
||||
mask_1 = torch.ones((b, h1, w1), dtype=torch.float32, device=img1.device)
|
||||
mask_p = torch.zeros((b, hp, wp), dtype=torch.float32, device=img1.device)
|
||||
mask_s = torch.zeros((b, strip.shape[1], strip.shape[2]), dtype=torch.float32, device=img1.device)
|
||||
|
||||
if direction == "right":
|
||||
return torch.cat([img1, strip, pair], dim=2)
|
||||
return torch.cat([pair, strip, img1], dim=2)
|
||||
return (
|
||||
torch.cat([img1, strip, pair], dim=2),
|
||||
torch.cat([mask_1, mask_s, mask_p], dim=2),
|
||||
)
|
||||
return (
|
||||
torch.cat([pair, strip, img1], dim=2),
|
||||
torch.cat([mask_p, mask_s, mask_1], dim=2),
|
||||
)
|
||||
|
||||
# --- 工具函数 ---
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class ImageMaskBlend(io.ComfyNode):
|
||||
category="1hewNodes/image/blend",
|
||||
inputs=[
|
||||
io.Image.Input("image"),
|
||||
io.Mask.Input("mask"),
|
||||
io.Mask.Input("mask", optional=True),
|
||||
io.Boolean.Input("fill_hole", default=True),
|
||||
io.Boolean.Input("invert", default=False),
|
||||
io.Int.Input("feather", default=0, min=0, max=50, step=1),
|
||||
@@ -47,7 +47,6 @@ class ImageMaskBlend(io.ComfyNode):
|
||||
async def execute(
|
||||
cls,
|
||||
image: torch.Tensor,
|
||||
mask: torch.Tensor,
|
||||
fill_hole: bool,
|
||||
invert: bool,
|
||||
feather: int,
|
||||
@@ -56,6 +55,7 @@ class ImageMaskBlend(io.ComfyNode):
|
||||
background_color: str,
|
||||
background_opacity: float,
|
||||
output_mask_invert: bool,
|
||||
mask: torch.Tensor | None = None,
|
||||
) -> io.NodeOutput:
|
||||
"""
|
||||
AE 对齐的遮罩融合(无 alpha 分支,输出 image 与 mask):
|
||||
@@ -76,6 +76,8 @@ class ImageMaskBlend(io.ComfyNode):
|
||||
"""
|
||||
# 规范化输入数据
|
||||
image = torch.clamp(image, min=0.0, max=1.0).to(torch.float32)
|
||||
if mask is None:
|
||||
mask = torch.zeros(image.shape[:-1], dtype=torch.float32, device=image.device)
|
||||
mask = torch.clamp(mask, min=0.0, max=1.0).to(torch.float32)
|
||||
device = image.device
|
||||
|
||||
|
||||
+14
-9
@@ -8,7 +8,7 @@ import folder_paths
|
||||
from comfy.cli_args import args
|
||||
from comfy_api.input import VideoInput
|
||||
from comfy_api.latest import io, ui
|
||||
from comfy_api.util import VideoCodec, VideoContainer
|
||||
from comfy_api.util import VideoContainer
|
||||
|
||||
|
||||
class SaveVideo(io.ComfyNode):
|
||||
@@ -18,12 +18,10 @@ class SaveVideo(io.ComfyNode):
|
||||
node_id="1hew_SaveVideo",
|
||||
display_name="Save Video",
|
||||
category="1hewNodes/io",
|
||||
description="保存视频到输出;支持空值输入。",
|
||||
inputs=[
|
||||
io.Video.Input("video", optional=True, tooltip="要保存的视频;为空时节点直接通过。"),
|
||||
io.String.Input("filename_prefix", default="video/ComfyUI", tooltip="保存文件前缀;支持格式占位符(如 %date:yyyy-MM-dd%)。"),
|
||||
io.Combo.Input("format", options=VideoContainer.as_input(), default="auto", tooltip="容器格式;auto 将自动选择合适格式。"),
|
||||
io.Combo.Input("codec", options=VideoCodec.as_input(), default="auto", tooltip="编码器;auto 将根据容器与内容自动选择。"),
|
||||
io.Boolean.Input("save_output", default=True, tooltip="是否保存到输出目录;若为 False 则保存到临时目录。"),
|
||||
],
|
||||
outputs=[],
|
||||
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
|
||||
@@ -35,12 +33,17 @@ class SaveVideo(io.ComfyNode):
|
||||
cls,
|
||||
video: Optional[VideoInput],
|
||||
filename_prefix: str,
|
||||
format: str,
|
||||
codec: str,
|
||||
save_output: bool,
|
||||
) -> io.NodeOutput:
|
||||
if video is None:
|
||||
return io.NodeOutput()
|
||||
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
output_type = io.FolderType.output
|
||||
if not save_output:
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
output_type = io.FolderType.temp
|
||||
|
||||
width, height = video.get_dimensions()
|
||||
(
|
||||
full_output_folder,
|
||||
@@ -49,7 +52,7 @@ class SaveVideo(io.ComfyNode):
|
||||
subfolder,
|
||||
filename_prefix,
|
||||
) = folder_paths.get_save_image_path(
|
||||
filename_prefix, folder_paths.get_output_directory(), width, height
|
||||
filename_prefix, output_dir, width, height
|
||||
)
|
||||
|
||||
saved_metadata = None
|
||||
@@ -62,11 +65,13 @@ class SaveVideo(io.ComfyNode):
|
||||
if len(metadata) > 0:
|
||||
saved_metadata = metadata
|
||||
|
||||
format = "auto"
|
||||
codec = "auto"
|
||||
extension = VideoContainer.get_extension(format)
|
||||
file = f"{filename}_{counter:05}_.{extension}"
|
||||
|
||||
path = os.path.join(full_output_folder, file)
|
||||
await asyncio.to_thread(video.save_to, path, format=format, codec=codec, metadata=saved_metadata)
|
||||
|
||||
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)])
|
||||
)
|
||||
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, output_type)])
|
||||
)
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import folder_paths
|
||||
import torch
|
||||
import torch.nn.functional as functional
|
||||
import torchaudio
|
||||
from comfy_api.latest import io, ui
|
||||
|
||||
_PATH_LOCK: Optional[asyncio.Lock] = None
|
||||
|
||||
class SaveVideoByImage(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="1hew_SaveVideoByImage",
|
||||
display_name="Save Video by Image",
|
||||
category="1hewNodes/io",
|
||||
inputs=[
|
||||
io.Image.Input("image"),
|
||||
io.Audio.Input("audio", optional=True),
|
||||
io.Float.Input("fps", default=8.0, min=0.01, max=120.0, step=0.01),
|
||||
io.String.Input("filename_prefix", default="video/ComfyUI"),
|
||||
io.Boolean.Input("save_output", default=True),
|
||||
],
|
||||
outputs=[],
|
||||
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
|
||||
is_output_node=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
image: torch.Tensor,
|
||||
fps: float,
|
||||
filename_prefix: str,
|
||||
save_output: bool,
|
||||
audio: Optional[dict] = None,
|
||||
) -> io.NodeOutput:
|
||||
images = image
|
||||
height, width, channels = images.shape[1:]
|
||||
has_alpha = channels == 4
|
||||
|
||||
if height % 2 != 0 or width % 2 != 0:
|
||||
new_height = height - (height % 2)
|
||||
new_width = width - (width % 2)
|
||||
images = cls._resize_images(images, new_height, new_width)
|
||||
height, width = new_height, new_width
|
||||
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
output_type = io.FolderType.output
|
||||
if not save_output:
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
output_type = io.FolderType.temp
|
||||
|
||||
global _PATH_LOCK
|
||||
if _PATH_LOCK is None:
|
||||
_PATH_LOCK = asyncio.Lock()
|
||||
|
||||
preview_file = ""
|
||||
preview_path = ""
|
||||
preview_subfolder = ""
|
||||
|
||||
async with _PATH_LOCK:
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
filename_prefix,
|
||||
) = folder_paths.get_save_image_path(
|
||||
filename_prefix, output_dir, width, height
|
||||
)
|
||||
|
||||
if has_alpha and save_output:
|
||||
output_file = f"{filename}_{counter:05}_.mov"
|
||||
path = os.path.join(full_output_folder, output_file)
|
||||
|
||||
preview_dir = os.path.join(
|
||||
folder_paths.get_temp_directory(),
|
||||
subfolder,
|
||||
)
|
||||
os.makedirs(preview_dir, exist_ok=True)
|
||||
preview_file = f"{filename}_{counter:05}_.webm"
|
||||
preview_path = os.path.join(preview_dir, preview_file)
|
||||
preview_subfolder = subfolder
|
||||
else:
|
||||
extension = "webm" if has_alpha else "mp4"
|
||||
file = f"{filename}_{counter:05}_.{extension}"
|
||||
path = os.path.join(full_output_folder, file)
|
||||
preview_file = file
|
||||
preview_path = path
|
||||
preview_subfolder = subfolder
|
||||
|
||||
if not os.path.exists(path):
|
||||
with open(path, "wb"):
|
||||
pass
|
||||
|
||||
if preview_path and preview_path != path:
|
||||
if not os.path.exists(preview_path):
|
||||
with open(preview_path, "wb"):
|
||||
pass
|
||||
|
||||
audio_path = None
|
||||
if audio is not None:
|
||||
try:
|
||||
waveform = audio.get("waveform")
|
||||
sample_rate = audio.get("sample_rate")
|
||||
if waveform is not None and sample_rate:
|
||||
if waveform.dim() == 3:
|
||||
waveform = waveform.squeeze(0)
|
||||
|
||||
audio_file = f"{filename}_{counter:05}_audio.wav"
|
||||
audio_path = os.path.join(
|
||||
folder_paths.get_temp_directory(),
|
||||
audio_file,
|
||||
)
|
||||
torchaudio.save(audio_path, waveform, sample_rate)
|
||||
except Exception as exc:
|
||||
print(
|
||||
"Info: Audio processing skipped; continuing without audio. "
|
||||
f"Details: {exc}"
|
||||
)
|
||||
audio_path = None
|
||||
|
||||
try:
|
||||
if has_alpha and save_output:
|
||||
await cls.ffmpeg_encode(
|
||||
images=images,
|
||||
path=preview_path,
|
||||
fps=fps,
|
||||
audio_path=audio_path,
|
||||
width=width,
|
||||
height=height,
|
||||
input_pix_fmt="rgba",
|
||||
video_codec="libvpx-vp9",
|
||||
output_pix_fmt="yuva420p",
|
||||
video_args=["-auto-alt-ref", "0", "-b:v", "0", "-crf", "30"],
|
||||
audio_codec="libopus",
|
||||
)
|
||||
await cls.ffmpeg_encode(
|
||||
images=images,
|
||||
path=path,
|
||||
fps=fps,
|
||||
audio_path=audio_path,
|
||||
width=width,
|
||||
height=height,
|
||||
input_pix_fmt="rgba",
|
||||
video_codec="prores_ks",
|
||||
output_pix_fmt="yuva444p10le",
|
||||
video_args=["-profile:v", "4"],
|
||||
audio_codec="aac",
|
||||
)
|
||||
else:
|
||||
await cls.ffmpeg_encode(
|
||||
images=images,
|
||||
path=path,
|
||||
fps=fps,
|
||||
audio_path=audio_path,
|
||||
width=width,
|
||||
height=height,
|
||||
input_pix_fmt="rgba" if has_alpha else "rgb24",
|
||||
video_codec="libvpx-vp9" if has_alpha else "libx264",
|
||||
output_pix_fmt="yuva420p" if has_alpha else "yuv420p",
|
||||
video_args=(
|
||||
["-auto-alt-ref", "0", "-b:v", "0", "-crf", "30"]
|
||||
if has_alpha
|
||||
else [
|
||||
"-preset",
|
||||
"medium",
|
||||
"-crf",
|
||||
"23",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
]
|
||||
),
|
||||
audio_codec="libopus" if has_alpha else "aac",
|
||||
)
|
||||
finally:
|
||||
if audio_path and os.path.exists(audio_path):
|
||||
os.remove(audio_path)
|
||||
|
||||
return io.NodeOutput(
|
||||
ui=ui.PreviewVideo(
|
||||
[
|
||||
ui.SavedResult(
|
||||
preview_file,
|
||||
preview_subfolder,
|
||||
(
|
||||
io.FolderType.temp
|
||||
if (has_alpha and save_output)
|
||||
else output_type
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def ffmpeg_encode(
|
||||
cls,
|
||||
images: torch.Tensor,
|
||||
path: str,
|
||||
fps: float,
|
||||
audio_path: Optional[str],
|
||||
width: int,
|
||||
height: int,
|
||||
input_pix_fmt: str,
|
||||
video_codec: str,
|
||||
output_pix_fmt: str,
|
||||
video_args: list[str],
|
||||
audio_codec: str,
|
||||
):
|
||||
ffmpeg_path = "ffmpeg"
|
||||
|
||||
command = [
|
||||
ffmpeg_path,
|
||||
"-y",
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-vcodec",
|
||||
"rawvideo",
|
||||
"-s",
|
||||
f"{width}x{height}",
|
||||
"-pix_fmt",
|
||||
input_pix_fmt,
|
||||
"-r",
|
||||
str(fps),
|
||||
"-i",
|
||||
"-",
|
||||
]
|
||||
|
||||
if audio_path:
|
||||
command.extend(["-i", audio_path])
|
||||
|
||||
command.extend(["-c:v", video_codec, "-pix_fmt", output_pix_fmt])
|
||||
command.extend(video_args)
|
||||
|
||||
if audio_path:
|
||||
command.extend(["-c:a", audio_codec, "-shortest"])
|
||||
|
||||
command.append(path)
|
||||
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*command,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
async def feed_stdin():
|
||||
for i in range(images.shape[0]):
|
||||
frame = images[i]
|
||||
frame = (frame * 255).byte().cpu().numpy()
|
||||
try:
|
||||
process.stdin.write(frame.tobytes())
|
||||
await process.stdin.drain()
|
||||
except (BrokenPipeError, OSError):
|
||||
break
|
||||
process.stdin.close()
|
||||
|
||||
async def log_stderr():
|
||||
stderr_data = b""
|
||||
while True:
|
||||
chunk = await process.stderr.read(4096)
|
||||
if not chunk:
|
||||
break
|
||||
stderr_data += chunk
|
||||
return stderr_data
|
||||
|
||||
_, stderr_output = await asyncio.gather(feed_stdin(), log_stderr())
|
||||
|
||||
await process.wait()
|
||||
|
||||
if process.returncode != 0:
|
||||
raise Exception(f"FFmpeg encoding failed: {stderr_output.decode()}")
|
||||
|
||||
except Exception as e:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def _resize_images(images: torch.Tensor, height: int, width: int) -> torch.Tensor:
|
||||
images_nchw = images.permute(0, 3, 1, 2).to(torch.float32)
|
||||
resized = functional.interpolate(
|
||||
images_nchw,
|
||||
size=(height, width),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
return resized.permute(0, 2, 3, 1).clamp(0.0, 1.0)
|
||||
@@ -1,234 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import av
|
||||
import asyncio
|
||||
import folder_paths
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import numpy as np
|
||||
import torch
|
||||
from fractions import Fraction
|
||||
from PIL import Image
|
||||
from typing import Optional
|
||||
from comfy_api.latest import io, ui
|
||||
|
||||
|
||||
class SaveVideoRGBA(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="1hew_SaveVideoRGBA",
|
||||
display_name="Save Video RGBA",
|
||||
category="1hewNodes/io",
|
||||
inputs=[
|
||||
io.Image.Input("images"),
|
||||
io.Float.Input(
|
||||
"fps", default=24.0, min=1.0, max=120.0, step=1.0
|
||||
),
|
||||
io.String.Input(
|
||||
"filename_prefix",
|
||||
default="video/ComfyUI",
|
||||
tooltip=(
|
||||
"The prefix for the file to save. This may include "
|
||||
"formatting information such as %date:yyyy-MM-dd% "
|
||||
"or %Empty Latent Image.width% to include values "
|
||||
"from nodes."
|
||||
),
|
||||
),
|
||||
io.Boolean.Input("only_preview", default=False),
|
||||
io.Audio.Input("audio", optional=True),
|
||||
],
|
||||
outputs=[],
|
||||
hidden=[io.Hidden.unique_id],
|
||||
is_output_node=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls, images, fps, filename_prefix, only_preview, audio=None, **kwargs
|
||||
) -> io.NodeOutput:
|
||||
b, h, w, c = images.shape
|
||||
has_alpha = c == 4
|
||||
|
||||
divisible_by = 2
|
||||
if w % divisible_by != 0 or h % divisible_by != 0:
|
||||
new_w = w - (w % divisible_by)
|
||||
new_h = h - (h % divisible_by)
|
||||
print(f"Resize video from {w}x{h} to {new_w}x{new_h}")
|
||||
|
||||
async def _resize_one(i):
|
||||
def _do():
|
||||
img = Image.fromarray(
|
||||
(images[i].cpu().numpy() * 255).astype(np.uint8)
|
||||
)
|
||||
resized = img.resize((new_w, new_h), Image.LANCZOS)
|
||||
arr = np.array(resized).astype(np.float32) / 255.0
|
||||
return torch.from_numpy(arr).unsqueeze(0)
|
||||
|
||||
return await asyncio.to_thread(_do)
|
||||
|
||||
tasks = [_resize_one(i) for i in range(b)]
|
||||
parts = await asyncio.gather(*tasks)
|
||||
images = torch.cat(parts, dim=0)
|
||||
b, h, w, c = images.shape
|
||||
|
||||
results = []
|
||||
|
||||
if only_preview or has_alpha:
|
||||
temp_dir = folder_paths.get_temp_directory()
|
||||
suffix = "".join(
|
||||
random.choice("abcdefghijklmnopqrstupvxyz") for _ in range(5)
|
||||
)
|
||||
prefix_append = f"ComfyUI_temp_{suffix}"
|
||||
(
|
||||
full_dir,
|
||||
base,
|
||||
counter,
|
||||
subfolder,
|
||||
filename_prefix,
|
||||
) = folder_paths.get_save_image_path(prefix_append, temp_dir, w, h)
|
||||
fmt = "webm" if has_alpha else "auto"
|
||||
ext = cls._get_extension(fmt)
|
||||
file = f"{base}_{counter:05}_.{ext}"
|
||||
await asyncio.to_thread(
|
||||
cls._save_video,
|
||||
images,
|
||||
audio,
|
||||
float(fps),
|
||||
os.path.join(full_dir, file),
|
||||
fmt,
|
||||
"auto",
|
||||
None,
|
||||
)
|
||||
results.append(ui.SavedResult(file, subfolder, io.FolderType.temp))
|
||||
|
||||
if not only_preview:
|
||||
out_dir = folder_paths.get_output_directory()
|
||||
(
|
||||
full_dir,
|
||||
base,
|
||||
counter,
|
||||
subfolder,
|
||||
filename_prefix,
|
||||
) = folder_paths.get_save_image_path(filename_prefix, out_dir, w, h)
|
||||
fmt = "mov" if has_alpha else "auto"
|
||||
codec = "prores_ks" if has_alpha else "auto"
|
||||
ext = cls._get_extension(fmt)
|
||||
file = f"{base}_{counter:05}_.{ext}"
|
||||
await asyncio.to_thread(
|
||||
cls._save_video,
|
||||
images,
|
||||
audio,
|
||||
float(fps),
|
||||
os.path.join(full_dir, file),
|
||||
fmt,
|
||||
codec,
|
||||
None,
|
||||
)
|
||||
if not has_alpha:
|
||||
results.append(ui.SavedResult(file, subfolder, io.FolderType.output))
|
||||
|
||||
return io.NodeOutput(ui=ui.PreviewVideo(results))
|
||||
|
||||
@staticmethod
|
||||
def _get_extension(fmt: str) -> str:
|
||||
if fmt == "auto":
|
||||
return "mp4"
|
||||
if fmt == "mp4":
|
||||
return "mp4"
|
||||
if fmt == "webm":
|
||||
return "webm"
|
||||
if fmt == "mov":
|
||||
return "mov"
|
||||
return "mp4"
|
||||
|
||||
@staticmethod
|
||||
def _save_video(
|
||||
images,
|
||||
audio: Optional[dict],
|
||||
fps: float,
|
||||
path: str,
|
||||
fmt: str,
|
||||
codec: str,
|
||||
metadata: Optional[dict],
|
||||
) -> None:
|
||||
has_alpha = images.shape[-1] == 4 if len(images.shape) == 4 else False
|
||||
|
||||
if has_alpha:
|
||||
if fmt == "auto":
|
||||
fmt = "webm"
|
||||
if codec == "auto":
|
||||
codec = "libvpx-vp9"
|
||||
if fmt not in ["webm", "mov"]:
|
||||
raise ValueError("Unsupported alpha format")
|
||||
else:
|
||||
if fmt != "auto" and fmt not in ["mp4", "webm", "mov"]:
|
||||
raise ValueError("Unsupported format")
|
||||
if codec == "auto":
|
||||
codec = "h264"
|
||||
elif codec not in ["h264", "libvpx-vp9", "prores_ks"]:
|
||||
raise ValueError("Unsupported codec")
|
||||
|
||||
options = {}
|
||||
fmt_str = None if fmt == "auto" else fmt
|
||||
if fmt in ["mp4", "mov"]:
|
||||
options["movflags"] = "use_metadata_tags"
|
||||
|
||||
with av.open(path, mode="w", format=fmt_str, options=options) as output:
|
||||
if metadata is not None:
|
||||
for k, v in metadata.items():
|
||||
output.metadata[k] = json.dumps(v)
|
||||
|
||||
rate = Fraction(round(fps * 1000), 1000)
|
||||
vstream = output.add_stream(codec, rate=rate)
|
||||
vstream.width = images.shape[2]
|
||||
vstream.height = images.shape[1]
|
||||
|
||||
if has_alpha:
|
||||
if codec in ["libvpx-vp9"]:
|
||||
vstream.pix_fmt = "yuva420p"
|
||||
elif codec in ["prores_ks"]:
|
||||
vstream.pix_fmt = "yuva444p10le"
|
||||
else:
|
||||
vstream.pix_fmt = "yuva420p"
|
||||
else:
|
||||
vstream.pix_fmt = "yuv420p"
|
||||
|
||||
astream = None
|
||||
asr = 1
|
||||
if audio:
|
||||
asr = int(audio["sample_rate"])
|
||||
acodec = "libopus" if fmt == "webm" else "aac"
|
||||
astream = output.add_stream(acodec, rate=asr)
|
||||
|
||||
for i, frame in enumerate(images):
|
||||
img = (frame * 255).clamp(0, 255).byte().cpu().numpy()
|
||||
if has_alpha:
|
||||
vf = av.VideoFrame.from_ndarray(img, format="rgba")
|
||||
vf = vf.reformat(format=vstream.pix_fmt)
|
||||
else:
|
||||
vf = av.VideoFrame.from_ndarray(img, format="rgb24")
|
||||
vf = vf.reformat(format=vstream.pix_fmt)
|
||||
pkt = vstream.encode(vf)
|
||||
output.mux(pkt)
|
||||
|
||||
pkt = vstream.encode(None)
|
||||
output.mux(pkt)
|
||||
|
||||
if astream and audio:
|
||||
waveform = audio["waveform"]
|
||||
frames = images.shape[0]
|
||||
total = math.ceil((asr / rate) * frames)
|
||||
waveform = waveform[:, :, :total]
|
||||
arr = waveform.movedim(2, 1).reshape(1, -1).float().numpy()
|
||||
af = av.AudioFrame.from_ndarray(
|
||||
arr,
|
||||
format="flt",
|
||||
layout="mono" if waveform.shape[1] == 1 else "stereo",
|
||||
)
|
||||
af.sample_rate = asr
|
||||
af.pts = 0
|
||||
output.mux(astream.encode(af))
|
||||
output.mux(astream.encode(None))
|
||||
@@ -145,6 +145,11 @@ class WorkflowName(io.ComfyNode):
|
||||
else:
|
||||
name_without_ext = file_name
|
||||
extension = ""
|
||||
|
||||
# 过滤非法文件名或特定系统文件名
|
||||
if name_without_ext.lower() in [".index", "index", ""] or name_without_ext.startswith("."):
|
||||
name_without_ext = "workflow"
|
||||
|
||||
processed_name = name_without_ext
|
||||
if prefix:
|
||||
processed_name = prefix + processed_name
|
||||
@@ -194,7 +199,7 @@ class WorkflowName(io.ComfyNode):
|
||||
date_str = now.strftime("%Y%m%d_%H%M%S")
|
||||
else:
|
||||
date_str = now.strftime("%Y-%m-%d")
|
||||
result = f"{date_str}/{result}"
|
||||
result = f"{date_str}/{name_without_ext}/{result}"
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"路径处理错误:{str(e)}"
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI-1hewNodes"
|
||||
description = "This is a custom node collection for ComfyUI that provides some utility nodes."
|
||||
version = "3.0.9"
|
||||
version = "3.1.0"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
# ImageMainStitch - Main stitch
|
||||
# Image Main Stitch - Main-image anchored stitching
|
||||
|
||||
**Node Purpose:** `ImageMainStitch` first builds a composition from `image_2..image_N` (horizontally for `top/bottom`, vertically for `left/right`), then attaches this composition to one side of `image_1` with optional spacing and size reconciliation.
|
||||
**Node Purpose:** `Image Main Stitch` stitches `image_2..image_N` into a group, then attaches the group to `image_1` along a chosen direction with optional spacing. It outputs a mask that marks the `image_1` region as `1` and the stitched-group/spacing region as `0`.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `image_1` | - | IMAGE | - | - | Primary image batch. |
|
||||
| `image_2` | - | IMAGE | - | - | Secondary image batch. |
|
||||
| `image_3` | - | IMAGE | - | - | Tertiary image batch. |
|
||||
| `direction` | - | COMBO | `left` | `top`/`bottom`/`left`/`right` | Side to attach the combined pair to `image_1`. |
|
||||
| `match_image_size` | - | BOOLEAN | True | - | When True, scales along the target axis; when False, pads to unify dimensions without resizing originals. |
|
||||
| `spacing_width` | - | INT | 10 | 0–1000 | Spacing width used for both the inner pair and the outer attachment. |
|
||||
| `spacing_color` | - | STRING | `1.0` | Gray/HEX/RGB | Spacing color. |
|
||||
| `pad_color` | - | STRING | `1.0` | Gray/HEX/RGB/`edge`/`average`/`extend`/`mirror` | Padding strategy when unifying sizes. |
|
||||
| `image_1` | - | IMAGE | - | - | Main image batch used as the anchor. |
|
||||
| `image_2` | optional | IMAGE | - | - | Additional image batch; stitched into the group. |
|
||||
| `image_3` | optional | IMAGE | - | - | Additional image batch; stitched into the group. |
|
||||
| `image_4..image_N` | optional | IMAGE | - | - | Dynamic extra inputs; discovered by name and appended in numeric order. |
|
||||
| `direction` | - | COMBO | `left` | `top` / `bottom` / `left` / `right` | Side on which the stitched group is placed relative to `image_1`. |
|
||||
| `match_image_size` | - | BOOLEAN | True | - | When True, uses aspect-preserving resize to match edge length for stitching; when False, uses centered padding to reconcile sizes. |
|
||||
| `spacing_width` | - | INT | 10 | 0–1000 | Spacing strip width/height between the main image and the group, and between images inside the group. |
|
||||
| `spacing_color` | - | STRING | `1.0` | Gray/HEX/RGB | Spacing strip color. |
|
||||
| `pad_color` | - | STRING | `1.0` | Gray/HEX/RGB/`edge`/`average`/`extend`/`mirror` | Padding strategy used when reconciling sizes. |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `image` | IMAGE | Stitched image batch.
|
||||
| `image` | IMAGE | Stitched image batch. |
|
||||
| `mask` | MASK | Region mask: `image_1` area is `1`, other areas are `0`. |
|
||||
|
||||
## Features
|
||||
|
||||
- Two-stage composition: builds `image_2+image_3` horizontally for `top/bottom` or vertically for `left/right`, then attaches to `image_1`.
|
||||
- Size reconciliation: `match_image_size=True` uses ratio-preserving resize on the target axis; otherwise uses centered padding or cropping.
|
||||
- Spacing control: single `spacing_width` applies to both pair and attachment stages.
|
||||
- Color strategies: supports edge-, average-, extend-, and mirror-based padding; spacing uses fixed RGB.
|
||||
- Batch broadcasting: repeats inputs to the maximum batch size for consistent output.
|
||||
- Dynamic inputs: accepts `image_2..image_N` and stitches them in ascending index order.
|
||||
- Two-stage layout: builds the group first, then attaches it to `image_1` based on `direction`.
|
||||
- Batch broadcasting: repeats smaller batches to the maximum batch size across all inputs.
|
||||
- Size reconciliation:
|
||||
- `match_image_size=True`: resizes with bicubic interpolation while preserving aspect ratio.
|
||||
- `match_image_size=False`: uses centered padding with `pad_color` for size alignment.
|
||||
- Color parsing:
|
||||
- `spacing_color`: grayscale, HEX, RGB, and named colors.
|
||||
- `pad_color`: adds `edge`, `average`, `extend`, `mirror` strategies.
|
||||
|
||||
## Typical Usage
|
||||
|
||||
- Side composition: set `direction=right` to attach the pair to the right of `image_1`.
|
||||
- Top/bottom stack: set `direction=top/bottom` to place the pair above/below with spacing.
|
||||
- Preserve originals: set `match_image_size=False` to avoid resizing and use padding.
|
||||
- Build a main canvas with reference strips: use `direction=left/right` to attach a vertical reference group.
|
||||
- Create a top/bottom comparison panel: use `direction=top/bottom` to attach a horizontal group.
|
||||
- Produce a “main region mask” for downstream compositing: use the `mask` output as a selector for operations applied only to `image_1`.
|
||||
|
||||
## Notes & Tips
|
||||
|
||||
- Internal helpers `_resize_keep_ratio` and `_pad_to_rgb` ensure natural scaling and padding.
|
||||
- When only `image_1` is provided, the node returns `image_1` and a full-white mask.
|
||||
- `spacing_width=0` yields a direct adjacency between regions.
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
# ImageMainStitch - 主画面拼接
|
||||
# Image Main Stitch - 主画面拼接
|
||||
|
||||
**节点功能**:`ImageMainStitch` 先将 `image_2..image_N` 顺序拼成组合(按方向选择水平或垂直),随后将该组合贴到 `image_1` 的指定侧。支持统一的 `spacing_width` 与尺寸匹配/填充策略。
|
||||
**节点功能:** `Image Main Stitch` 节点以 `image_1` 为主画面锚点,将 `image_2..image_N` 先拼接成组合,再按 `direction` 与主画面合并,并可添加间隔条。输出的 `mask` 将 `image_1` 区域标记为 `1`,组合与间隔区域标记为 `0`。
|
||||
|
||||
## Inputs | 输入
|
||||
## 输入
|
||||
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `image_1` | - | IMAGE | - | - | 主图像批次 |
|
||||
| `image_2` | - | IMAGE | - | - | 次图像批次 |
|
||||
| `image_3` | - | IMAGE | - | - | 三图像批次 |
|
||||
| `direction` | - | COMBO | `left` | `top`/`bottom`/`left`/`right` | 将组合贴到 `image_1` 的哪一侧 |
|
||||
| `match_image_size` | - | BOOLEAN | True | - | `True` 沿目标轴等比缩放;`False` 通过居中填充统一尺寸,不改变原图 |
|
||||
| `spacing_width` | - | INT | 10 | 0–1000 | 同时用于 2+3 合并与最终贴合的间隔宽度 |
|
||||
| `spacing_color` | - | STRING | `1.0` | 灰度/HEX/RGB | 间隔条颜色 |
|
||||
| `pad_color` | - | STRING | `1.0` | 灰度/HEX/RGB/`edge`/`average`/`extend`/`mirror` | 尺寸统一时的填充策略 |
|
||||
| `image_1` | 必需 | IMAGE | - | - | 主画面图像批次,作为拼接锚点。 |
|
||||
| `image_2` | 可选 | IMAGE | - | - | 追加图像批次,参与组合拼接。 |
|
||||
| `image_3` | 可选 | IMAGE | - | - | 追加图像批次,参与组合拼接。 |
|
||||
| `image_4..image_N` | 可选 | IMAGE | - | - | 动态追加输入,按编号顺序收集并拼接。 |
|
||||
| `direction` | - | COMBO | `left` | `top` / `bottom` / `left` / `right` | 组合相对 `image_1` 的摆放方向。 |
|
||||
| `match_image_size` | - | BOOLEAN | True | - | 为 True 时按等比缩放匹配拼接边长;为 False 时按居中填充对齐尺寸。 |
|
||||
| `spacing_width` | - | INT | 10 | 0–1000 | 主画面与组合之间、组合内部图像之间的间隔条宽度/高度。 |
|
||||
| `spacing_color` | - | STRING | `1.0` | 灰度/HEX/RGB | 间隔条颜色。 |
|
||||
| `pad_color` | - | STRING | `1.0` | 灰度/HEX/RGB/`edge`/`average`/`extend`/`mirror` | 尺寸对齐时的填充策略。 |
|
||||
|
||||
## Outputs | 输出
|
||||
## 输出
|
||||
|
||||
| 输出名称 | 数据类型 | 描述 |
|
||||
|---------|----------|------|
|
||||
| `image` | IMAGE | 拼接后的图像批次 |
|
||||
| `image` | IMAGE | 拼接后的图像批次。 |
|
||||
| `mask` | MASK | 区域遮罩:`image_1` 区域为 `1`,其余区域为 `0`。 |
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 两阶段合成:`top/bottom` 下 2 与 3 先水平并排,再与 1 垂直拼接;`left/right` 下先垂直堆叠,再与 1 水平拼接。
|
||||
- 尺寸统一:`match_image_size=True` 沿目标轴等比缩放;否则进行居中填充。
|
||||
- 动态输入:支持 `image_2..image_N`,并按编号从小到大顺序拼接。
|
||||
- 两阶段布局:先构建组合,再按 `direction` 与主画面合并。
|
||||
- 批次广播:输入批次大小自动扩展到最大批次,便于批处理。
|
||||
- 尺寸对齐:
|
||||
- `match_image_size=True`:使用双三次插值等比缩放并保持比例。
|
||||
- `match_image_size=False`:使用 `pad_color` 进行居中填充对齐尺寸。
|
||||
- 颜色解析:
|
||||
- `spacing_color`:支持灰度、HEX、RGB 与常见颜色名。
|
||||
- `pad_color`:在此基础上支持 `edge`、`average`、`extend`、`mirror` 策略。
|
||||
|
||||
## 典型用法
|
||||
|
||||
- 主画面配参考条:使用 `direction=left/right` 将竖向参考组合贴到主画面侧边。
|
||||
- 顶部/底部对比栏:使用 `direction=top/bottom` 将横向组合贴到主画面上方或下方。
|
||||
- 主区域遮罩输出:将 `mask` 作为选择器,配合后续节点仅对主画面区域执行处理。
|
||||
|
||||
## 注意与建议
|
||||
|
||||
- 仅连接 `image_1` 时,节点输出 `image_1` 与全白 `mask`。
|
||||
- `spacing_width=0` 时区域之间直接贴合。
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `image` | - | IMAGE | - | - | Input image batch. |
|
||||
| `mask` | - | MASK | - | - | Input mask batch; resized to match `image` size when needed. |
|
||||
| `mask` | optional | MASK | - | - | Input mask batch; resized to match `image` size when needed. |
|
||||
| `fill_hole` | - | BOOLEAN | true | - | Fill mask holes to ensure continuous shapes. |
|
||||
| `invert` | - | BOOLEAN | false | - | Invert the selection after morphology/feather steps. |
|
||||
| `feather` | - | INT | 0 | 0–50 | Gaussian blur radius applied to the mask. |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `image` | - | IMAGE | - | - | 输入图像批次 |
|
||||
| `mask` | - | MASK | - | - | 输入遮罩批次;必要时自动调整到与图像匹配的尺寸 |
|
||||
| `mask` | 可选 | MASK | - | - | 输入遮罩批次;必要时自动调整到与图像匹配的尺寸 |
|
||||
| `fill_hole` | - | BOOLEAN | true | - | 填补遮罩孔洞,保障连续形状 |
|
||||
| `invert` | - | BOOLEAN | false | - | 在形态/羽化完成后反转选择区域 |
|
||||
| `feather` | - | INT | 0 | 0–50 | 对遮罩进行高斯羽化,平滑边缘 |
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Match Brightness Contrast
|
||||
# Match Brightness Contrast - Match brightness and contrast
|
||||
|
||||
**Node Purpose:** Adjusts the brightness and contrast of the source image to match the reference image. Supports matching methods based on histogram or standard statistics, with control over the calculation area (full image or edges).
|
||||
**Node Purpose:** `Match Brightness Contrast` adjusts the brightness and contrast of `source_image` to match `reference_image`. It supports histogram matching and standard mean/std matching, plus optional edge-only statistics and sequence consistency modes for batch workflows.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `source_image` | Required | IMAGE | - | - | Source image batch to be adjusted |
|
||||
| `reference_image` | Required | IMAGE | - | - | Reference image batch providing brightness and contrast info |
|
||||
| `edge_amount` | - | FLOAT | 0.2 | 0.0-8192.0 | Calculation area control. <=0: Full image; <1.0: Edge percentage; >=1.0: Edge pixel width |
|
||||
| `consistency` | - | COMBO | `lock_first` | `lock_first` / `frame_match` | Temporal consistency. `lock_first`: Lock first frame params for sequence; `frame_match`: Match frame by frame |
|
||||
| `method` | - | COMBO | `histogram` | `standard` / `histogram` | Matching algorithm. `histogram`: Histogram matching (more precise); `standard`: Mean/Std matching (softer) |
|
||||
| `source_image` | - | IMAGE | - | - | Source image batch to be adjusted. |
|
||||
| `reference_image` | - | IMAGE | - | - | Reference image batch that provides target tone distribution. |
|
||||
| `edge_amount` | - | FLOAT | 0.2 | 0.0-8192.0 | Statistics area control. `<=0`: full image; `<1.0`: edge ratio based on the shorter side; `>=1.0`: edge width in pixels. |
|
||||
| `consistency` | - | COMBO | `lock_first` | `lock_first` / `lock_mid` / `lock_end` / `frame_match` | Sequence consistency. `lock_*` computes parameters once from a selected pair and applies to all source frames; `frame_match` computes per frame. |
|
||||
| `method` | - | COMBO | `histogram` | `standard` / `histogram` | Matching method. `histogram` uses per-channel CDF mapping; `standard` uses mean/std linear remap. |
|
||||
|
||||
## Outputs
|
||||
|
||||
@@ -26,16 +26,18 @@
|
||||
- **Area Control (`edge_amount`)**:
|
||||
- 0: Statistics from the full image.
|
||||
- >0: Statistics only from the peripheral edge areas, ignoring the center. Useful for matching ambient atmosphere while ignoring subject differences.
|
||||
- **Temporal Consistency (`consistency`)**:
|
||||
- `lock_first`: Calculates matching parameters only from the first frame pair and applies them to all subsequent frames. Ideal for video processing to prevent flickering.
|
||||
- `frame_match`: Calculates matching parameters for each frame individually. Suitable for unrelated image batches.
|
||||
- **Sequence Consistency (`consistency`)**:
|
||||
- `lock_first`: Computes parameters from `source_image[0]` and `reference_image[0]`.
|
||||
- `lock_mid`: Computes parameters from the middle frame of each input batch.
|
||||
- `lock_end`: Computes parameters from the last frame of each input batch.
|
||||
- `frame_match`: Computes parameters for each frame pair (`reference_image[i % ref_batch]`).
|
||||
|
||||
## Typical Usage
|
||||
|
||||
- **Video Style Unification**: Connect video frames to `source_image` and a style reference to `reference_image`, set `consistency=lock_first` to unify video tone with the reference stably.
|
||||
- **Video Tone Unification**: Connect video frames to `source_image` and a tone reference sequence to `reference_image`, set `consistency=lock_mid` or `lock_end` to keep batch tone stable when the early reference frames vary.
|
||||
- **Compositing Blending**: For image compositing, use foreground as `source_image` and background as `reference_image`, set appropriate `edge_amount` (e.g., 0.2) to blend foreground edges with the background tone.
|
||||
|
||||
## Notes & Tips
|
||||
|
||||
- Always use `lock_first` for video processing to ensure stability.
|
||||
- Proper use of `edge_amount` prevents subject colors (e.g., clothing) from interfering with the overall tone matching.
|
||||
- For sequence workflows, `lock_*` keeps a single mapping across frames, which helps keep tone consistent.
|
||||
- `edge_amount` focuses statistics on the border region and supports stable background tone matching.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Match Brightness Contrast - 匹配亮度与对比度
|
||||
|
||||
**节点功能:** 调整源图像的亮度和对比度以匹配参考图像。支持基于直方图或标准统计信息的匹配方法,并可控制计算区域(全图或边缘)。
|
||||
**节点功能:** `Match Brightness Contrast` 节点用于将 `source_image` 的亮度与对比度映射到 `reference_image` 的色调分布。支持直方图匹配与均值/标准差匹配,并提供边缘区域统计与序列一致性策略,适合批处理与视频帧序列。
|
||||
|
||||
## 输入
|
||||
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `source_image` | 必需 | IMAGE | - | - | 需要调整的源图像批次 |
|
||||
| `reference_image` | 必需 | IMAGE | - | - | 提供亮度与对比度参考的图像批次 |
|
||||
| `edge_amount` | - | FLOAT | 0.2 | 0.0-8192.0 | 计算区域控制。<=0: 全图; <1.0: 边缘百分比; >=1.0: 边缘像素宽度 |
|
||||
| `consistency` | - | COMBO | `lock_first` | `lock_first` / `frame_match` | 时序一致性控制。`lock_first`: 锁定首帧参数应用于全序列; `frame_match`: 逐帧匹配 |
|
||||
| `method` | - | COMBO | `histogram` | `standard` / `histogram` | 匹配算法。`histogram`: 直方图匹配 (更精确); `standard`: 均值/标准差匹配 (更柔和) |
|
||||
| `reference_image` | 必需 | IMAGE | - | - | 提供目标色调分布的参考图像批次 |
|
||||
| `edge_amount` | - | FLOAT | 0.2 | 0.0-8192.0 | 统计区域控制。`<=0`:全图;`<1.0`:按短边比例取边缘宽度;`>=1.0`:按像素取边缘宽度 |
|
||||
| `consistency` | - | COMBO | `lock_first` | `lock_first` / `lock_mid` / `lock_end` / `frame_match` | 序列一致性。`lock_*` 从选定帧对计算一次参数并应用到全部源帧;`frame_match` 逐帧计算 |
|
||||
| `method` | - | COMBO | `histogram` | `standard` / `histogram` | 匹配方法。`histogram` 通过通道 CDF 映射;`standard` 通过均值/标准差线性映射 |
|
||||
|
||||
## 输出
|
||||
|
||||
@@ -26,16 +26,18 @@
|
||||
- **区域控制 (`edge_amount`)**:
|
||||
- 当值为 0 时,统计全图信息。
|
||||
- 当值大于 0 时,仅统计图像四周边缘区域的信息,忽略中心内容。这对于忽略主体差异、仅匹配环境氛围非常有用。
|
||||
- **时序一致性 (`consistency`)**:
|
||||
- `lock_first`:仅计算第一帧源图与参考图的匹配参数,并应用到后续所有帧。适合视频处理,避免因画面内容变化导致的闪烁。
|
||||
- `frame_match`:每一帧都单独计算匹配参数。适合处理不相关的单图批次。
|
||||
- **序列一致性 (`consistency`)**:
|
||||
- `lock_first`:使用 `source_image[0]` 与 `reference_image[0]` 计算参数。
|
||||
- `lock_mid`:使用输入批次的中间帧计算参数。
|
||||
- `lock_end`:使用输入批次的末帧计算参数。
|
||||
- `frame_match`:逐帧计算参数(参考帧按 `reference_image[i % ref_batch]` 取值)。
|
||||
|
||||
## 典型用法
|
||||
|
||||
- **视频风格统一**:连接视频帧到 `source_image`,风格参考图到 `reference_image`,设置 `consistency=lock_first`,使视频整体色调与参考图一致且保持稳定。
|
||||
- **视频色调统一**:连接视频帧到 `source_image`,参考序列到 `reference_image`,设置 `consistency=lock_mid` 或 `lock_end`,在参考序列前段存在偏移时保持映射稳定。
|
||||
- **图像合成融合**:在图像合成时,将前景图作为 `source_image`,背景图作为 `reference_image`,设置适当的 `edge_amount` (如 0.2),使前景边缘的色调与背景融合。
|
||||
|
||||
## 注意与建议
|
||||
|
||||
- 处理视频时务必使用 `lock_first` 以保证画面稳定性。
|
||||
- `edge_amount` 的合理使用可以避免主体颜色(如人物衣服)对整体色调匹配的干扰。
|
||||
- 序列流程使用 `lock_*` 可将单一映射应用到全部源帧,有助于保持色调一致。
|
||||
- `edge_amount` 将统计集中在边缘区域,可用于背景/环境氛围的稳定匹配。
|
||||
|
||||
@@ -1,39 +1,32 @@
|
||||
# Save Video - Save to Output with Preview
|
||||
# Save Video - Save VideoInput to disk
|
||||
|
||||
**Node Purpose:** `Save Video` saves the input video to the output folder using a selected container and codec. When the input is empty, the node completes without side effects. It provides a preview entry in the UI.
|
||||
**Node Purpose:** `Save Video` saves a `VideoInput` into the output or temp directory and returns a UI preview. It supports optional input and can pass through when the input is empty.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `video` | optional | VIDEO | - | - | Video to save; when absent, the node passes through. |
|
||||
| `filename_prefix` | - | STRING | `video/ComfyUI` | - | Save path prefix; supports formatting placeholders handled by the framework. |
|
||||
| `format` | - | COMBO | `auto` | container options | Container format selection; `auto` chooses a suitable format. |
|
||||
| `codec` | - | COMBO | `auto` | codec options | Codec selection; `auto` chooses based on container and content. |
|
||||
| `video` | optional | VIDEO | - | - | Video to save; when absent, the node completes with no output. |
|
||||
| `filename_prefix` | - | STRING | `video/ComfyUI` | - | Save prefix; supports format placeholders (e.g., `%date:yyyy-MM-dd%`). |
|
||||
| `save_output` | - | BOOLEAN | True | - | When True, saves into the output directory; when False, saves into the temp directory. |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| - | - | Output node; returns a UI preview element for the saved video. |
|
||||
| - | - | Output node; returns a UI preview of the saved video. |
|
||||
|
||||
## Features
|
||||
|
||||
- Safe no-input behavior: completes immediately when `video` is not provided.
|
||||
- Path computation: uses `get_save_image_path` to compute folder, filename, counter, and subfolder.
|
||||
- Metadata handling: includes prompt and extra info unless metadata is disabled.
|
||||
- Extension resolution: derives extension from selected container.
|
||||
- Async saving: saves via `video.save_to` on a background thread.
|
||||
- UI preview: returns `PreviewVideo` with saved result info.
|
||||
- Output node: marked as an output node and hides prompt/extra PNG info in the interface.
|
||||
- Optional input handling: `video` can be omitted for conditional workflows.
|
||||
- Auto container selection: uses `format=auto` and `codec=auto` to pick a suitable output format.
|
||||
- Metadata embedding: when enabled in ComfyUI, writes prompt and extra PNG info as metadata.
|
||||
|
||||
## Typical Usage
|
||||
|
||||
- Choose container and codec: set `format` and `codec` explicitly for target platforms.
|
||||
- Prefix organization: use `filename_prefix` to route outputs into subfolders like `video/ComfyUI`.
|
||||
- Batch-friendly naming: rely on the auto-increment `counter` in filename to avoid collisions.
|
||||
- Save an upstream decoded video: connect a VIDEO output into `video` and set `filename_prefix`.
|
||||
- Conditional save: drive `video` with an optional branch so the node saves only when a valid video is provided.
|
||||
|
||||
## Notes & Tips
|
||||
|
||||
- The filename pattern is `{filename}_{counter:05}_.{extension}`; customize `filename_prefix` to guide folder and base name.
|
||||
- When metadata is enabled, prompt and extra info are saved alongside the video.
|
||||
- `save_output=False` is useful for preview-oriented workflows that keep artifacts in the temp directory.
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
# Save Video - 视频保存与预览
|
||||
# Save Video - 保存视频
|
||||
|
||||
**节点功能:** `Save Video` 将输入视频保存到输出目录,支持选择容器与编码器;当输入为空时直接完成并不产生副作用。保存后在界面提供预览入口。
|
||||
**节点功能:** `Save Video` 节点将 `VideoInput` 保存到输出目录或临时目录,并在界面中返回预览结果。节点支持可选输入,适合条件分支流程。
|
||||
|
||||
## 输入
|
||||
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `video` | 可选 | VIDEO | - | - | 待保存的视频;为空时节点直接通过。 |
|
||||
| `filename_prefix` | - | STRING | `video/ComfyUI` | - | 保存路径前缀;支持由框架处理的格式占位符。 |
|
||||
| `format` | - | COMBO | `auto` | 容器选项 | 容器格式选择;`auto` 自动选择合适格式。 |
|
||||
| `codec` | - | COMBO | `auto` | 编码器选项 | 编码器选择;`auto` 根据容器与内容自动选择。 |
|
||||
| `video` | 可选 | VIDEO | - | - | 要保存的视频;输入为空时节点直接完成流程。 |
|
||||
| `filename_prefix` | - | STRING | `video/ComfyUI` | - | 保存文件前缀;支持格式占位符(如 `%date:yyyy-MM-dd%`)。 |
|
||||
| `save_output` | - | BOOLEAN | True | - | 为 True 时保存到输出目录;为 False 时保存到临时目录。 |
|
||||
|
||||
## 输出
|
||||
|
||||
| 输出名称 | 数据类型 | 描述 |
|
||||
|---------|----------|------|
|
||||
| - | - | 输出节点;返回保存视频的界面预览元素。 |
|
||||
| - | - | 输出节点;在界面中返回保存结果的视频预览。 |
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 空输入安全:当未提供 `video` 时直接完成。
|
||||
- 适配批处理:依赖文件名中的自增 `counter` 避免同名冲突。
|
||||
- 可选输入:`video` 支持按需连接,便于条件分支与可选保存。
|
||||
- 自动格式选择:使用 `format=auto` 与 `codec=auto` 选择适合的输出格式。
|
||||
- 元数据写入:在 ComfyUI 允许写入元数据时,保存 prompt 与 extra 信息到视频元数据。
|
||||
|
||||
## 典型用法
|
||||
|
||||
- 保存上游视频:连接 VIDEO 输出到 `video`,设置 `filename_prefix` 进行归档。
|
||||
- 条件保存:在分支逻辑中按需提供 `video`,实现仅在满足条件时保存。
|
||||
|
||||
## 注意与建议
|
||||
|
||||
- 文件名模式为 `{filename}_{counter:05}_.{extension}`;通过设定 `filename_prefix` 引导目录与基本名。
|
||||
- 启用元数据时,提示与额外信息会与视频一并保存。
|
||||
- `save_output=False` 适合以预览为主的流程,将文件保存在临时目录中。
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Save Video by Image - Encode image batches into video
|
||||
|
||||
**Node Purpose:** `Save Video by Image` encodes an IMAGE batch into a video file at the given FPS. It supports optional AUDIO muxing, alpha-aware codec/container selection, and returns a preview in the UI.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `image` | - | IMAGE | - | - | Input image batch `[B,H,W,C]` in 0–1 range. |
|
||||
| `audio` | optional | AUDIO | - | - | Optional audio dict containing `waveform` and `sample_rate`. |
|
||||
| `fps` | - | FLOAT | 8.0 | 0.01–120.0 | Frames per second for the output video. |
|
||||
| `filename_prefix` | - | STRING | `video/ComfyUI` | - | Save path prefix under output or temp directory. |
|
||||
| `save_output` | - | BOOLEAN | True | - | When True, saves into the output directory; when False, saves into the temp directory. |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| - | - | Output node; returns a UI preview of the saved video. |
|
||||
|
||||
## Features
|
||||
|
||||
- Dimension alignment: auto-resizes to even width/height for video encoding compatibility.
|
||||
- Alpha handling:
|
||||
- RGBA inputs (`C=4`) use alpha-capable encoding.
|
||||
- When `save_output=True` with RGBA, the node saves a `.mov` for final output and a `.webm` for UI preview.
|
||||
- Audio muxing: when `audio` is provided, the node writes a temporary WAV and muxes it into the final container.
|
||||
- FFmpeg piping: streams raw frames to FFmpeg via stdin for efficient encoding.
|
||||
|
||||
## Typical Usage
|
||||
|
||||
- Export a frame sequence as MP4: connect an IMAGE batch, set `fps`, keep `save_output=True`.
|
||||
- Export an RGBA sequence: use RGBA images, set `save_output=True` to generate a `.mov` plus a `.webm` preview.
|
||||
- Add audio: connect an AUDIO input from upstream nodes, and the node muxes audio into the result.
|
||||
|
||||
## Notes & Tips
|
||||
|
||||
- Ensure `ffmpeg` is available on the system PATH.
|
||||
- Large batches increase encoding time; use a suitable FPS and batch length for workflow needs.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Save Video by Image - 图像序列保存为视频
|
||||
|
||||
**节点功能:** `Save Video by Image` 节点将 IMAGE 批次编码为视频文件,并支持可选的 AUDIO 合成。节点具备 Alpha 感知的编码与容器选择策略,并在界面中返回视频预览结果。
|
||||
|
||||
## 输入
|
||||
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `image` | 必需 | IMAGE | - | - | 输入图像批次 `[B,H,W,C]`,像素范围为 0–1。 |
|
||||
| `audio` | 可选 | AUDIO | - | - | 可选音频输入,包含 `waveform` 与 `sample_rate`。 |
|
||||
| `fps` | - | FLOAT | 8.0 | 0.01–120.0 | 输出视频帧率。 |
|
||||
| `filename_prefix` | - | STRING | `video/ComfyUI` | - | 保存路径前缀,位于输出目录或临时目录下。 |
|
||||
| `save_output` | - | BOOLEAN | True | - | 为 True 时保存到输出目录;为 False 时保存到临时目录。 |
|
||||
|
||||
## 输出
|
||||
|
||||
| 输出名称 | 数据类型 | 描述 |
|
||||
|---------|----------|------|
|
||||
| - | - | 输出节点;在界面中返回保存结果的视频预览。 |
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 尺寸对齐:自动调整为偶数宽高,以适配常见视频编码约束。
|
||||
- Alpha 支持:
|
||||
- RGBA 输入(`C=4`)使用支持透明通道的编码方式。
|
||||
- `save_output=True` 且为 RGBA 时,输出目录保存 `.mov`,同时生成 `.webm` 作为界面预览。
|
||||
- 音频合成:提供 `audio` 时,节点生成临时 WAV 并合成到目标视频容器。
|
||||
- FFmpeg 管线:通过 stdin 方式向 FFmpeg 传输原始帧数据进行编码。
|
||||
|
||||
## 典型用法
|
||||
|
||||
- 将帧序列导出为 MP4:连接 IMAGE 批次,设置 `fps`,保持 `save_output=True`。
|
||||
- 导出带透明通道序列:输入 RGBA 图像,设置 `save_output=True` 生成 `.mov` 并提供 `.webm` 预览。
|
||||
- 合成音频:连接上游 AUDIO,节点将音频合成到输出视频中。
|
||||
|
||||
## 注意与建议
|
||||
|
||||
- 系统环境中准备可用的 `ffmpeg` 命令。
|
||||
- 批次数量与分辨率影响编码耗时,可按工作流需求选择合适的帧率与帧数。
|
||||
@@ -1,40 +0,0 @@
|
||||
# Save Video RGBA - Save RGB/RGBA batches with alpha-safe codecs
|
||||
|
||||
**Node Purpose:** `Save Video RGBA` saves an image batch as a video with support for both RGB and RGBA inputs. It ensures dimensions are divisible by 2, selects alpha-safe containers/codecs, and returns a preview entry. When alpha is present, it produces a `webm` preview and writes a production `mov` (ProRes 4444) to the output folder.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `images` | - | IMAGE | - | - | Image batch `B×H×W×C` (`C=3`/`4`). |
|
||||
| `fps` | - | FLOAT | `24.0` | `1.0–120.0` (step `1.0`) | Output frame rate. |
|
||||
| `filename_prefix` | - | STRING | `video/ComfyUI` | - | Save path prefix; supports formatting placeholders handled by the framework. |
|
||||
| `only_preview` | - | BOOLEAN | `False` | - | Save to temporary preview only; skips regular output entry when enabled. |
|
||||
| `audio` | optional | AUDIO | - | - | Audio track to mux; uses `libopus` for `webm` and `aac` for `mp4/mov`. |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| - | - | Output node; returns a UI preview element with saved result info. |
|
||||
|
||||
## Features
|
||||
|
||||
- Divisible-by-2 resize: when width or height is not divisible by 2, resizes frames down using LANCZOS to satisfy codec requirements.
|
||||
- Alpha-aware formats: with alpha, preview uses `webm` + `libvpx-vp9` and production output uses `mov` + `prores_ks` (ProRes 4444, `yuva444p10le`). Without alpha, defaults to `mp4` + `h264`.
|
||||
- Pixel formats: selects `yuva420p` for VP9, `yuva444p10le` for ProRes with alpha, and `yuv420p` for RGB-only outputs.
|
||||
- Path and naming: computes folder, base name, and counter via `get_save_image_path`; final filename pattern `{base}_{counter:05}_.{ext}`.
|
||||
- Async save: performs encoding on a worker thread and returns `PreviewVideo` with saved entries.
|
||||
- Audio mux: resamples and crops audio to match video duration; uses `mono`/`stereo` layout based on channels.
|
||||
|
||||
## Typical Usage
|
||||
|
||||
- Preserve transparency: feed RGBA batches; preview uses `webm` (alpha), and the output folder contains a `mov` suitable for professional workflows.
|
||||
- Preview-only exports: enable `only_preview` to generate a temporary video without adding a regular output entry.
|
||||
- Deterministic naming: rely on the auto-incrementing counter to organize repeated runs under `filename_prefix`.
|
||||
|
||||
## Notes & Tips
|
||||
|
||||
- Alpha workflow: preview favors `webm` for broad browser compatibility; production output uses `mov` with ProRes 4444 for high-fidelity alpha.
|
||||
- Audio handling: when provided, audio is cropped to match the video frame count at the specified `fps`.
|
||||
- Layout and range: expects channels-last tensors; frames are converted to 8-bit per channel during encoding with values clamped to `[0,1]` in preprocessing.
|
||||
@@ -1,40 +0,0 @@
|
||||
# Save Video RGBA - 支持透明通道的视频保存
|
||||
|
||||
**节点功能:** `Save Video RGBA` 将图像批次保存为视频,支持 RGB 与 RGBA 输入。它保证输出尺寸满足 2 的倍数、自动选择适合透明通道的容器与编码器,并在界面返回预览。存在透明通道时,会生成 `webm` 预览,并在输出目录写入 `mov`(ProRes 4444)。
|
||||
|
||||
## 输入
|
||||
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `images` | - | IMAGE | - | - | 图像批次 `B×H×W×C`(`C=3/4`)。 |
|
||||
| `fps` | - | FLOAT | `24.0` | `1.0–120.0`(步长 `1.0`) | 输出帧率。 |
|
||||
| `filename_prefix` | - | STRING | `video/ComfyUI` | - | 保存路径前缀;支持由框架处理的格式占位符。 |
|
||||
| `only_preview` | - | BOOLEAN | `False` | - | 开启后仅生成临时预览文件。 |
|
||||
| `audio` | 可选 | AUDIO | - | - | 音频轨道;`webm` 使用 `libopus`,`mp4/mov` 使用 `aac`。 |
|
||||
|
||||
## 输出
|
||||
|
||||
| 输出名称 | 数据类型 | 描述 |
|
||||
|---------|----------|------|
|
||||
| - | - | 输出节点;返回包含保存结果信息的界面预览元素。 |
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 尺寸整除保证:当宽或高不满足 2 的倍数时,使用 LANCZOS 下采样到最近的可编码尺寸。
|
||||
- 透明通道工作流:存在透明通道时,预览采用 `webm` + `libvpx-vp9`,正式导出采用 `mov` + `prores_ks`(ProRes 4444,`yuva444p10le`)。无透明通道时默认 `mp4` + `h264`。
|
||||
- 像素格式选择:VP9 使用 `yuva420p`;ProRes 透明使用 `yuva444p10le`;RGB 输出使用 `yuv420p`。
|
||||
- 路径与命名:通过 `get_save_image_path` 计算目录、基本名与计数器;文件名模式为 `{base}_{counter:05}_.{ext}`。
|
||||
- 异步保存:编码在工作线程执行,并返回 `PreviewVideo` 以展示保存条目。
|
||||
- 音频复用:按帧数裁剪并复用音轨;依据通道数采用 `mono`/`stereo` 布局。
|
||||
|
||||
## 典型用法
|
||||
|
||||
- 保留透明:输入 RGBA 批次;预览生成 `webm`(支持透明),输出目录包含适用于专业流程的 `mov`。
|
||||
- 仅预览导出:启用 `only_preview` 生成临时视频,便于快速检查与对比。
|
||||
- 有序命名:依赖自增计数器在 `filename_prefix` 指定目录下组织批量导出。
|
||||
|
||||
## 注意与建议
|
||||
|
||||
- 透明通道流程:预览侧选择 `webm` 以适配浏览器展示;正式导出采用 ProRes 4444,适合高保真后期环节。
|
||||
- 音频时长对齐:提供音频时,会按指定 `fps` 对齐并裁剪至视频时长。
|
||||
- 数据布局与范围:输入为末通道张量;编码前按 `[0,1]` 浮点范围进行裁剪与 8 位转换。
|
||||
@@ -1,39 +1,36 @@
|
||||
# Workflow Name - Current Workflow Path/Name with Date
|
||||
# Workflow Name - Derive workflow filename
|
||||
|
||||
**Node Purpose:** `Workflow Name` reads the current workflow path from a monitored temp file and formats the output with optional directory prefix for date, plus configurable `prefix`/`suffix` and extension handling.
|
||||
**Node Purpose:** `Workflow Name` reads the current workflow path recorded by the workflow monitor and returns a processed name string. It supports prefix/suffix, date-based folder formatting, full-path output, and optional extension stripping.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `prefix` | - | STRING | `` | - | Text prepended to the workflow name. |
|
||||
| `suffix` | - | STRING | `` | - | Text appended to the workflow name. |
|
||||
| `date_format` | - | COMBO | `yyyy-MM-dd` | many | Date folder prefix; set to `none` to disable. |
|
||||
| `full_path` | - | BOOLEAN | `False` | - | Output full path (dir + name) or only file name. |
|
||||
| `strip_extension` | - | BOOLEAN | `True` | - | Remove `.json` extension from the output name. |
|
||||
| `prefix` | - | STRING | - | - | Prefix appended to the workflow name. |
|
||||
| `suffix` | - | STRING | - | - | Suffix appended to the workflow name. |
|
||||
| `date_format` | - | COMBO | `yyyy-MM-dd` | `none` / `yyyy-MM-dd` / `yyyy/MM/dd` / `yyyyMMdd` / `yyyy-MM-dd HH:mm` / `yyyy/MM/dd HH:mm` / `yyyy-MM-dd HH:mm:ss` / `MM-dd` / `MM/dd` / `MMdd` / `dd` / `HH:mm` / `HH:mm:ss` / `yyyy年MM月dd日` / `MM月dd日` / `yyyyMMdd_HHmm` / `yyyyMMdd_HHmmss` | Date folder format used to build the final string. |
|
||||
| `full_path` | - | BOOLEAN | False | - | When True, includes the directory part of the workflow path (normalized with `/`). |
|
||||
| `strip_extension` | - | BOOLEAN | True | - | When True, removes the `.json` extension from the returned name. |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `string` | STRING | Formatted workflow path or name, optionally prefixed by date. |
|
||||
| `string` | STRING | Processed workflow name string. |
|
||||
|
||||
## Features
|
||||
|
||||
- Monitored temp file: reads `current_workflow.tmp` from candidate locations under `custom_nodes`.
|
||||
- Retry-on-lock: retries on `PermissionError` with small delays.
|
||||
- Path processing: normalize slashes, choose full path or basename, handle `.json` extension.
|
||||
- Naming controls: apply `prefix`/`suffix`, optional extension stripping.
|
||||
- Date prefix: adds a date directory based on selected format.
|
||||
- Dynamic fingerprint: ensures fresh evaluation via a time-based fingerprint.
|
||||
- Monitor integration: reads `current_workflow.tmp` produced by the workflow monitor utility.
|
||||
- Name processing: extracts base filename (or full path), applies prefix/suffix, and optionally strips `.json`.
|
||||
- Date folder building: when `date_format` is enabled, returns `date_str/original_name/result`.
|
||||
- Robust file reading: retries several times to handle concurrent file writes.
|
||||
|
||||
## Typical Usage
|
||||
|
||||
- Full path with date: keep `full_path=True`, `strip_extension=True`, and `date_format=yyyy-MM-dd` to organize saved results under dated folders.
|
||||
- Name only: set `full_path=False` to get just the file name with your `prefix`/`suffix`.
|
||||
- Preserve extension: set `strip_extension=False` when downstream consumers expect `.json`.
|
||||
- Use as `filename_prefix` for save nodes: combine with image/video saving to organize outputs by workflow and date.
|
||||
- Produce stable naming across sessions: set `strip_extension=True` and select a deterministic `date_format`.
|
||||
|
||||
## Notes & Tips
|
||||
|
||||
- Ensure the monitoring script writes to `current_workflow.tmp` so the node can read the latest workflow path.
|
||||
- When `date_format=none`, the output omits the date folder while retaining other formatting choices.
|
||||
- The workflow monitor writes `current_workflow.tmp`; keep the monitor running for continuous updates.
|
||||
- `full_path=True` returns forward-slash normalized paths for cross-platform consistency.
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
# Workflow Name - 当前工作流路径/名称。
|
||||
# Workflow Name - 工作流名称
|
||||
|
||||
**节点功能:** `Workflow Name` 节点读取工作流监控脚本记录的当前工作流路径,并输出处理后的名称字符串。支持前后缀、日期目录格式、完整路径输出,以及扩展名处理选项。
|
||||
|
||||
## 输入
|
||||
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `prefix` | - | STRING | - | - | 输出名称前缀。 |
|
||||
| `suffix` | - | STRING | - | - | 输出名称后缀。 |
|
||||
| `date_format` | - | COMBO | `yyyy-MM-dd` | `none` / `yyyy-MM-dd` / `yyyy/MM/dd` / `yyyyMMdd` / `yyyy-MM-dd HH:mm` / `yyyy/MM/dd HH:mm` / `yyyy-MM-dd HH:mm:ss` / `MM-dd` / `MM/dd` / `MMdd` / `dd` / `HH:mm` / `HH:mm:ss` / `yyyy年MM月dd日` / `MM月dd日` / `yyyyMMdd_HHmm` / `yyyyMMdd_HHmmss` | 日期目录格式,用于构建输出字符串。 |
|
||||
| `full_path` | - | BOOLEAN | False | - | 为 True 时输出包含目录部分,并将路径分隔符统一为 `/`。 |
|
||||
| `strip_extension` | - | BOOLEAN | True | - | 为 True 时移除 `.json` 扩展名。 |
|
||||
|
||||
## 输出
|
||||
|
||||
| 输出名称 | 数据类型 | 描述 |
|
||||
|---------|----------|------|
|
||||
| `string` | STRING | 处理后的工作流名称字符串。 |
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 监控集成:读取工作流监控脚本生成的 `current_workflow.tmp`。
|
||||
- 名称处理:提取文件名(或完整路径),应用 `prefix`/`suffix`,并按需处理 `.json` 扩展名。
|
||||
- 日期目录:启用 `date_format` 时输出结构为 `date_str/原始工作流名/结果`。
|
||||
- 稳定读取:在文件写入并发场景下进行多次尝试读取。
|
||||
|
||||
## 典型用法
|
||||
|
||||
- 含日期的完整路径:保持 `full_path=True`、`strip_extension=True`、`date_format=yyyy-MM-dd`,便于将结果归档到日期目录下。
|
||||
- 仅名称:设置 `full_path=False` 以获得纯文件名,同时使用 `prefix`/`suffix` 做定制。
|
||||
- 保留扩展:当下游需要 `.json` 时设置 `strip_extension=False`。
|
||||
- 作为保存节点的 `filename_prefix`:与图像/视频保存节点结合,实现按工作流与日期归档输出。
|
||||
- 输出可复用命名:设置 `strip_extension=True` 并选择稳定的 `date_format`,获得结构化路径。
|
||||
|
||||
## 注意与建议
|
||||
|
||||
- 请确保监控脚本将最新工作流路径写入 `current_workflow.tmp`,以便节点读取。
|
||||
- 当 `date_format=none` 时输出不含日期目录,其他命名控制仍然生效。
|
||||
- 工作流监控脚本持续运行时,`current_workflow.tmp` 会保持更新。
|
||||
- `full_path=True` 适合需要目录层级信息的归档策略。
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app } from "../../../scripts/app.js";
|
||||
app.registerExtension({
|
||||
name: "ComfyUI-1hewNodesV3.SaveVideoInfo",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name !== "1hew_SaveVideo") {
|
||||
if (nodeData.name !== "1hew_SaveVideo" && nodeData.name !== "1hew_SaveVideoByImage") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,18 @@ app.registerExtension({
|
||||
if (this.widgets) {
|
||||
for (const w of this.widgets) {
|
||||
if (w.element) {
|
||||
let videoEl = w.element;
|
||||
if (videoEl.tagName !== "VIDEO") {
|
||||
videoEl = w.element.querySelector("video");
|
||||
let videoEls = [];
|
||||
if (w.element.tagName === "VIDEO") {
|
||||
videoEls = [w.element];
|
||||
} else {
|
||||
videoEls = Array.from(
|
||||
w.element.querySelectorAll("video")
|
||||
);
|
||||
}
|
||||
|
||||
if (videoEl) {
|
||||
|
||||
for (const videoEl of videoEls) {
|
||||
attachDimensionDisplay(videoEl);
|
||||
return;
|
||||
applyLoopedHoverAudioPreview(videoEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +44,41 @@ app.registerExtension({
|
||||
}
|
||||
});
|
||||
|
||||
function applyLoopedHoverAudioPreview(videoEl) {
|
||||
if (!videoEl || videoEl.dataset.comfy1hewPreviewApplied === "1") {
|
||||
return;
|
||||
}
|
||||
videoEl.dataset.comfy1hewPreviewApplied = "1";
|
||||
|
||||
videoEl.autoplay = true;
|
||||
videoEl.loop = true;
|
||||
videoEl.muted = true;
|
||||
videoEl.playsInline = true;
|
||||
videoEl.controls = false;
|
||||
videoEl.preload = "auto";
|
||||
|
||||
const safePlay = () => {
|
||||
const p = videoEl.play();
|
||||
if (p && typeof p.catch === "function") {
|
||||
p.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
safePlay();
|
||||
videoEl.addEventListener("loadeddata", safePlay);
|
||||
videoEl.addEventListener("canplay", safePlay);
|
||||
|
||||
videoEl.addEventListener("pointerenter", () => {
|
||||
videoEl.muted = false;
|
||||
videoEl.volume = 1.0;
|
||||
safePlay();
|
||||
});
|
||||
|
||||
videoEl.addEventListener("pointerleave", () => {
|
||||
videoEl.muted = true;
|
||||
});
|
||||
}
|
||||
|
||||
function attachDimensionDisplay(videoEl) {
|
||||
// 1. 获取或创建容器
|
||||
// ComfyUI 的视频通常直接放在 widget.element 中,或者包裹在一个 div 中
|
||||
|
||||
Reference in New Issue
Block a user