Skip to content

Commit f270934

Browse files
authored
Merge branch 'master' into feat/partner-nodes/bytedance-vcube
2 parents d12a59d + 76135e5 commit f270934

8 files changed

Lines changed: 852 additions & 74 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Notify on Merge
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
notify:
10+
runs-on: ubuntu-latest
11+
if: github.repository == 'Comfy-Org/ComfyUI'
12+
steps:
13+
- name: Notify downstream
14+
env:
15+
DISPATCH_TOKEN: ${{ secrets.SYNC_DISPATCH_TOKEN }}
16+
TARGET_REPO: ${{ secrets.SYNC_TARGET_REPO }}
17+
COMMIT_SHA: ${{ github.sha }}
18+
run: |
19+
set -euo pipefail
20+
if [ -z "${DISPATCH_TOKEN:-}" ] || [ -z "${TARGET_REPO:-}" ]; then
21+
echo "::notice::SYNC_DISPATCH_TOKEN/SYNC_TARGET_REPO not set; skipping downstream notify."
22+
exit 0
23+
fi
24+
PAYLOAD="$(jq -n --arg sha "$COMMIT_SHA" \
25+
'{ event_type: "upstream-push", client_payload: { sha: $sha } }')"
26+
curl -fsSL --connect-timeout 10 --max-time 60 -X POST \
27+
-H "Accept: application/vnd.github+json" \
28+
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
29+
"https://api.github.com/repos/${TARGET_REPO}/dispatches" \
30+
-d "$PAYLOAD"

comfy_api/latest/_input/video_types.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,24 @@ def save_to(
3030
metadata: Optional[dict] = None,
3131
bit_depth: int | None = None,
3232
crf: float | None = None,
33+
color_space: str | None = None,
3334
):
3435
"""
3536
Abstract method to save the video input to a file.
3637
3738
bit_depth selects the encoded bit depth; None keeps the video's native depth.
38-
crf selects the H.264 constant rate factor; None uses the encoder default.
39+
crf selects the H.264 or AV1 constant rate factor; None uses the encoder default.
40+
color_space="sRGB" writes SDR BT.709/sRGB video. "HDR" writes 10-bit BT.2020/HLG video;
41+
"HDR PQ" selects BT.2020/PQ.
42+
Tensor-created videos default to sRGB when color_space is None. Loaded videos keep matching recognized native color
43+
properties; other input pixels must already use the selected color space.
3944
"""
4045
pass
4146

47+
def get_color_space(self) -> str:
48+
"""Return the video's color space as sRGB, HDR, HDR PQ, or auto when unspecified."""
49+
return "auto"
50+
4251
@abstractmethod
4352
def as_trimmed(
4453
self,

comfy_api/latest/_input_impl/video_types.py

Lines changed: 186 additions & 41 deletions
Large diffs are not rendered by default.

comfy_api/latest/_util/video_types.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
class VideoCodec(str, Enum):
88
AUTO = "auto"
99
H264 = "h264"
10+
AV1 = "av1"
1011

1112
@classmethod
1213
def as_input(cls) -> list[str]:
@@ -18,6 +19,8 @@ def as_input(cls) -> list[str]:
1819
class VideoContainer(str, Enum):
1920
AUTO = "auto"
2021
MP4 = "mp4"
22+
MKV = "mkv"
23+
WEBM = "webm"
2124

2225
@classmethod
2326
def as_input(cls) -> list[str]:
@@ -35,6 +38,10 @@ def get_extension(cls, value) -> str:
3538
value = cls(value)
3639
if value == VideoContainer.MP4 or value == VideoContainer.AUTO:
3740
return "mp4"
41+
if value == VideoContainer.MKV:
42+
return "mkv"
43+
if value == VideoContainer.WEBM:
44+
return "webm"
3845
return ""
3946

4047
@dataclass

comfy_extras/nodes_video.py

Lines changed: 103 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,77 @@ def execute(cls, images, codec, fps, filename_prefix, crf) -> io.NodeOutput:
7272

7373
return io.NodeOutput(images, ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
7474

75+
def _save_video_color_space_input():
76+
return io.Combo.Input(
77+
"color_space",
78+
options=["auto", "sRGB", "HDR", "HDR PQ"],
79+
default="auto",
80+
display_name="color space",
81+
tooltip="Auto uses sRGB for videos created from images and preserves recognized colors on loaded videos. sRGB writes SDR BT.709/sRGB. HDR writes 10-bit BT.2020/HLG; HDR PQ writes BT.2020/PQ. Other input pixels must already use the selected color space.",
82+
)
83+
84+
85+
def _save_video_codec_input(supported_codecs: list[str], *, optional=False, hidden=False):
86+
codec_options = []
87+
if "auto" in supported_codecs:
88+
codec_options.append(io.DynamicCombo.Option("auto", []))
89+
if "h264" in supported_codecs:
90+
codec_options.append(
91+
io.DynamicCombo.Option(
92+
"h264",
93+
[
94+
io.DynamicCombo.Input(
95+
"encoding",
96+
display_name="encoding mode",
97+
options=[
98+
io.DynamicCombo.Option("auto", []),
99+
io.DynamicCombo.Option(
100+
"re-encode",
101+
[
102+
io.Float.Input("crf", default=23.0, min=0.0, max=51.0, step=1.0, tooltip="Lower values produce higher quality and larger files."),
103+
_save_video_color_space_input(),
104+
],
105+
),
106+
],
107+
optional=True,
108+
tooltip="Automatic preserves compatible H.264 streams. Re-encode applies custom encoding options.",
109+
),
110+
],
111+
)
112+
)
113+
if "av1" in supported_codecs:
114+
codec_options.append(
115+
io.DynamicCombo.Option(
116+
"av1",
117+
[
118+
io.DynamicCombo.Input(
119+
"encoding",
120+
display_name="encoding mode",
121+
options=[
122+
io.DynamicCombo.Option("auto", []),
123+
io.DynamicCombo.Option(
124+
"re-encode",
125+
[
126+
io.Float.Input("crf", default=30.0, min=0.0, max=63.0, step=1.0, tooltip="Lower values produce higher quality and larger files."),
127+
_save_video_color_space_input(),
128+
],
129+
),
130+
],
131+
optional=True,
132+
tooltip="Automatic preserves compatible AV1 streams. Re-encode applies custom encoding options.",
133+
),
134+
],
135+
)
136+
)
137+
return io.DynamicCombo.Input(
138+
"codec",
139+
options=codec_options,
140+
optional=optional,
141+
tooltip="The output video codec. Auto preserves a compatible source stream. H.264 and AV1 re-encoding support SDR, HDR (HLG), and HDR PQ.",
142+
extra_dict={"hidden": True} if hidden else None,
143+
)
144+
145+
75146
class SaveVideo(io.ComfyNode):
76147
@classmethod
77148
def define_schema(cls):
@@ -85,42 +156,37 @@ def define_schema(cls):
85156
inputs=[
86157
io.Video.Input("video", tooltip="The video to save."),
87158
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."),
88-
io.Combo.Input("format", options=Types.VideoContainer.as_input(), default="auto", tooltip="The format to save the video as."),
89159
io.DynamicCombo.Input(
90-
"codec",
160+
"format",
91161
options=[
92-
io.DynamicCombo.Option("auto", []),
93-
io.DynamicCombo.Option(
94-
"h264",
95-
[
96-
io.DynamicCombo.Input(
97-
"encoding",
98-
display_name="encoding mode",
99-
options=[
100-
io.DynamicCombo.Option("auto", []),
101-
io.DynamicCombo.Option(
102-
"re-encode",
103-
[io.Float.Input("crf", default=23.0, min=0.0, max=51.0, step=1.0, tooltip="Lower values produce higher quality and larger files.")],
104-
),
105-
],
106-
optional=True,
107-
tooltip="Automatic preserves compatible H.264 streams. Re-encode applies a custom CRF.",
108-
),
109-
],
110-
),
162+
io.DynamicCombo.Option("auto", [_save_video_codec_input(["auto", "h264", "av1"])]),
163+
io.DynamicCombo.Option("mp4", [_save_video_codec_input(["auto", "h264", "av1"])]),
164+
io.DynamicCombo.Option("mkv", [_save_video_codec_input(["auto", "h264", "av1"])]),
165+
io.DynamicCombo.Option("webm", [_save_video_codec_input(["auto", "av1"])]),
111166
],
112-
tooltip="The codec to use for the video.",
167+
tooltip="The output container. Auto preserves the source container when possible; MP4, MKV, and WebM select a specific container.",
113168
),
169+
_save_video_codec_input(["auto", "h264", "av1"], optional=True, hidden=True),
114170
],
115171
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
116172
is_output_node=True,
117-
outputs=[io.Video.Output("video")],
173+
outputs=[io.Video.Output("video", tooltip="The input video, unchanged.")],
118174
)
119175

120176
@classmethod
121-
def execute(cls, video: Input.Video, filename_prefix, format: str, codec: io.DynamicCombo.Type) -> io.NodeOutput:
177+
def execute(cls, video: Input.Video, filename_prefix, format: io.DynamicCombo.Type | str, codec: io.DynamicCombo.Type | None = None) -> io.NodeOutput:
178+
if isinstance(format, dict):
179+
format_name = format["format"]
180+
codec = format.get("codec") or codec
181+
else:
182+
format_name = format
183+
if codec is None:
184+
codec = {"codec": "auto"}
122185
codec_name = codec["codec"]
123186
encoding = codec.get("encoding") or {}
187+
color_space = encoding.get("color_space")
188+
if color_space == "auto":
189+
color_space = None
124190
width, height = video.get_dimensions()
125191
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
126192
filename_prefix,
@@ -137,13 +203,14 @@ def execute(cls, video: Input.Video, filename_prefix, format: str, codec: io.Dyn
137203
metadata["prompt"] = cls.hidden.prompt
138204
if len(metadata) > 0:
139205
saved_metadata = metadata
140-
file = f"{filename}_{counter:05}_.{Types.VideoContainer.get_extension(format)}"
206+
file = f"{filename}_{counter:05}_.{Types.VideoContainer.get_extension(format_name)}"
141207
video.save_to(
142208
os.path.join(full_output_folder, file),
143-
format=Types.VideoContainer(format),
144-
codec=codec_name,
209+
format=Types.VideoContainer(format_name),
210+
codec=Types.VideoCodec(codec_name),
145211
metadata=saved_metadata,
146212
crf=encoding.get("crf"),
213+
color_space=color_space,
147214
)
148215

149216
return io.NodeOutput(video, ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
@@ -199,7 +266,7 @@ def define_schema(cls):
199266
search_aliases=["extract frames", "split video", "video to images", "demux"],
200267
display_name="Get Video Components",
201268
category="video",
202-
description="Extracts all components from a video: frames, audio, framerate, and bit depth.",
269+
description="Extracts video frames, audio, frame rate, bit depth, and color space.",
203270
inputs=[
204271
io.Video.Input("video", tooltip="The video to extract components from."),
205272
],
@@ -208,13 +275,20 @@ def define_schema(cls):
208275
io.Audio.Output(display_name="audio"),
209276
io.Float.Output(display_name="fps"),
210277
io.Int.Output(display_name="bit_depth"),
278+
io.Combo.Output(display_name="color_space"),
211279
],
212280
)
213281

214282
@classmethod
215283
def execute(cls, video: Input.Video) -> io.NodeOutput:
216284
components = video.get_components()
217-
return io.NodeOutput(components.images, components.audio, float(components.frame_rate), video.get_bit_depth())
285+
return io.NodeOutput(
286+
components.images,
287+
components.audio,
288+
float(components.frame_rate),
289+
video.get_bit_depth(),
290+
video.get_color_space(),
291+
)
218292

219293

220294
class LoadVideo(io.ComfyNode):

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
comfyui-frontend-package==1.49.6
2-
comfyui-workflow-templates==0.11.43
2+
comfyui-workflow-templates==0.11.44
33
comfyui-embedded-docs==0.5.10
44
torch
55
torchsde

tests-unit/comfy_api_test/input_impl_test.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from comfy_api.input_impl.video_types import (
33
container_to_output_format,
44
get_open_write_kwargs,
5+
video_encoder_options,
56
)
6-
from comfy_api.util import VideoContainer
7+
from comfy_api.util import VideoCodec, VideoContainer
78

89

910
def test_container_to_output_format_empty_string():
@@ -36,7 +37,7 @@ def test_get_open_write_kwargs_filepath_no_format():
3637
kwargs_specific = get_open_write_kwargs("output.avi", "mp4", "avi")
3738
fail_msg = "Format should not be set for file paths (Specific)"
3839
assert "format" not in kwargs_specific, fail_msg
39-
assert kwargs_specific["options"]["movflags"] == "use_metadata_tags"
40+
assert "options" not in kwargs_specific
4041

4142

4243
def test_get_open_write_kwargs_base_options_mode():
@@ -90,3 +91,16 @@ def test_get_open_write_kwargs_bytesio_specific_format_list():
9091

9192
fail_msg = "Format should be a valid format from the specified format list when output format is not AUTO"
9293
assert kwargs["format"] in to_fmt, fail_msg
94+
95+
96+
def test_get_open_write_kwargs_does_not_pass_movflags_to_matroska_or_webm():
97+
for format, suffix in ((VideoContainer.MKV, "mkv"), (VideoContainer.WEBM, "webm")):
98+
assert "options" not in get_open_write_kwargs(f"output.{suffix}", "mp4", format)
99+
assert "options" not in get_open_write_kwargs(io.BytesIO(), "mp4", format)
100+
101+
102+
def test_av1_zero_crf_uses_lossless_mode():
103+
assert video_encoder_options(VideoCodec.AV1, 0) == {"svtav1-params": "lossless=1"}
104+
assert video_encoder_options(VideoCodec.AV1, 30.0) == {"crf": "30.0"}
105+
assert video_encoder_options(VideoCodec.H264, 0) == {"crf": "0"}
106+
assert video_encoder_options(VideoCodec.AV1, None) == {}

0 commit comments

Comments
 (0)