Compare commits

...

3 Commits

Author SHA1 Message Date
Octopus
35ae6029e1
Merge 66c4fde38c into 025e6792ee 2026-05-03 23:14:07 +08:00
octo-patch
66c4fde38c fix: ensure SaveWEBM container is always closed on error (fixes #9115)
Wrap the av.Container operations in SaveWEBM.execute with try/finally
to guarantee container.close() is called even when an exception occurs
during encoding (e.g. OOM, codec error). Without this, the output file
handle could remain open, causing 'file is open in Python' errors on
Windows when trying to delete or overwrite the output file.

Consistent with the pattern already used in encode_single_frame() and
decode_single_frame() in comfy_extras/nodes_lt.py.
2026-04-11 13:25:41 +08:00
octo-patch
f4d4dfaa66 fix: change SaveAnimatedWEBP default method from 'default' to 'fastest' (fixes #13300)
The 'default' compression method maps to WebP method=4, which is significantly
slower than method=0 ('fastest'). For animated WebP with many frames (e.g. 120
frames of video), this resulted in encoding times of 2+ minutes.

Changing the node default to 'fastest' (method=0) reduces encoding time by ~3x
while still allowing users to select higher compression methods when needed.
2026-04-09 12:33:21 +08:00
2 changed files with 23 additions and 21 deletions

View File

@ -196,7 +196,7 @@ class SaveAnimatedWEBP(IO.ComfyNode):
IO.Float.Input("fps", default=6.0, min=0.01, max=1000.0, step=0.01),
IO.Boolean.Input("lossless", default=True),
IO.Int.Input("quality", default=80, min=0, max=100),
IO.Combo.Input("method", options=list(cls.COMPRESS_METHODS.keys())),
IO.Combo.Input("method", options=list(cls.COMPRESS_METHODS.keys()), default="fastest"),
# "num_frames": ("INT", {"default": 0, "min": 0, "max": 8192}),
],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],

View File

@ -39,29 +39,31 @@ class SaveWEBM(io.ComfyNode):
file = f"{filename}_{counter:05}_.webm"
container = av.open(os.path.join(full_output_folder, file), mode="w")
if cls.hidden.prompt is not None:
container.metadata["prompt"] = json.dumps(cls.hidden.prompt)
try:
if cls.hidden.prompt is not None:
container.metadata["prompt"] = json.dumps(cls.hidden.prompt)
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
container.metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
container.metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
codec_map = {"vp9": "libvpx-vp9", "av1": "libsvtav1"}
stream = container.add_stream(codec_map[codec], rate=Fraction(round(fps * 1000), 1000))
stream.width = images.shape[-2]
stream.height = images.shape[-3]
stream.pix_fmt = "yuv420p10le" if codec == "av1" else "yuv420p"
stream.bit_rate = 0
stream.options = {'crf': str(crf)}
if codec == "av1":
stream.options["preset"] = "6"
codec_map = {"vp9": "libvpx-vp9", "av1": "libsvtav1"}
stream = container.add_stream(codec_map[codec], rate=Fraction(round(fps * 1000), 1000))
stream.width = images.shape[-2]
stream.height = images.shape[-3]
stream.pix_fmt = "yuv420p10le" if codec == "av1" else "yuv420p"
stream.bit_rate = 0
stream.options = {'crf': str(crf)}
if codec == "av1":
stream.options["preset"] = "6"
for frame in images:
frame = av.VideoFrame.from_ndarray(torch.clamp(frame[..., :3] * 255, min=0, max=255).to(device=torch.device("cpu"), dtype=torch.uint8).numpy(), format="rgb24")
for packet in stream.encode(frame):
container.mux(packet)
container.mux(stream.encode())
container.close()
for frame in images:
frame = av.VideoFrame.from_ndarray(torch.clamp(frame[..., :3] * 255, min=0, max=255).to(device=torch.device("cpu"), dtype=torch.uint8).numpy(), format="rgb24")
for packet in stream.encode(frame):
container.mux(packet)
container.mux(stream.encode())
finally:
container.close()
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))