From 04be690a15bc9542eb334f8b46f83ee67b526b4a Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 9 Jul 2026 13:41:06 +0200 Subject: [PATCH] Add GPU (NVENC AV1) webm format and save_latent toggle to FastAbsoluteSaver - nvenc_av1-webm: GPU-encoded webm via av1_nvenc (NVENC has no VP9 encoder); uses libopus since webm rejects AAC audio. - save_latent boolean gates the latent sidecar write while keeping the latent passthrough intact. Co-Authored-By: Claude Opus 4.8 --- fast_saver.py | 14 ++++++++++---- tests/test_fast_saver_latent.py | 14 +++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/fast_saver.py b/fast_saver.py index 43c1a0d..477ad84 100644 --- a/fast_saver.py +++ b/fast_saver.py @@ -139,6 +139,10 @@ VIDEO_FORMATS = { "nvenc_av1-mp4": {"ext": ".mp4", "codec": ["-c:v", "av1_nvenc"], "quality": "bitrate", "color_mgmt": True, "acodec": "aac", "extra": ["-movflags", "+faststart"]}, + # GPU webm: NVENC has no VP9 encoder, so use AV1 (a valid WebM codec) on the GPU. + # WebM containers only allow Opus/Vorbis audio, not AAC. + "nvenc_av1-webm":{"ext": ".webm", "codec": ["-c:v", "av1_nvenc"], + "quality": "bitrate", "color_mgmt": True, "acodec": "libopus"}, } @@ -164,7 +168,8 @@ class FastAbsoluteSaver: # --- FORMAT SWITCH --- "save_format": (["png", "webp", "mp4", "webm", "h265-mp4", "av1-mp4", "gif", - "ffv1-mkv", "prores-mov", "nvenc_h264-mp4", "nvenc_hevc-mp4", "nvenc_av1-mp4"], ), + "ffv1-mkv", "prores-mov", "nvenc_h264-mp4", "nvenc_hevc-mp4", "nvenc_av1-mp4", + "nvenc_av1-webm"], ), # --- NAMING CONTROL --- "use_timestamp": ("BOOLEAN", {"default": False, "label": "Add Timestamp (Unique)"}), @@ -176,6 +181,7 @@ class FastAbsoluteSaver: "metadata_key": ("STRING", {"default": "sharpness_score"}), "save_workflow_metadata": ("BOOLEAN", {"default": False, "label": "Save ComfyUI Workflow (Graph)"}), "save_metadata_png": ("BOOLEAN", {"default": False, "label": "Embed Workflow in PNG (sidecar or first file)"}), + "save_latent": ("BOOLEAN", {"default": True, "label": "Save Latent Sidecar"}), # --- PERFORMANCE --- "max_threads": ("INT", {"default": 0, "min": 0, "max": 128, "step": 1, "label": "Max Threads (0=Auto)"}), @@ -504,7 +510,7 @@ class FastAbsoluteSaver: return out_file def save_images_fast(self, images, output_path, filename_prefix, save_format, use_timestamp, auto_increment, counter_digits, - max_threads, filename_with_score, metadata_key, save_workflow_metadata, save_metadata_png, + max_threads, filename_with_score, metadata_key, save_workflow_metadata, save_metadata_png, save_latent, webp_lossless, webp_quality, webp_method, video_fps, video_crf, video_pixel_format, video_bitrate, prores_profile, gif_dither, @@ -538,7 +544,7 @@ class FastAbsoluteSaver: extra_data=extra_pnginfo, bitrate=video_bitrate, prores_profile=prores_profile, gif_dither=gif_dither, audio=audio) - if latent is not None: + if latent is not None and save_latent: self._save_latent_sidecar(latent, out_file) # Save metadata sidecar PNG next to the video file if save_metadata_png: @@ -612,7 +618,7 @@ class FastAbsoluteSaver: if future.result(): saved_image_paths.append(full_path) - if latent is not None: + if latent is not None and save_latent: for image_path in saved_image_paths: self._save_latent_sidecar(latent, image_path) diff --git a/tests/test_fast_saver_latent.py b/tests/test_fast_saver_latent.py index 7b301e1..f4b80f3 100644 --- a/tests/test_fast_saver_latent.py +++ b/tests/test_fast_saver_latent.py @@ -4,7 +4,7 @@ import torch from fast_saver import FastAbsoluteSaver -def _save_args(tmp_path, *, save_format="png", latent=None): +def _save_args(tmp_path, *, save_format="png", latent=None, save_latent=True): return { "images": torch.zeros((1, 2, 2, 3), dtype=torch.float32), "output_path": str(tmp_path), @@ -18,6 +18,7 @@ def _save_args(tmp_path, *, save_format="png", latent=None): "metadata_key": "sharpness_score", "save_workflow_metadata": False, "save_metadata_png": False, + "save_latent": save_latent, "webp_lossless": True, "webp_quality": 100, "webp_method": 4, @@ -55,6 +56,17 @@ def test_png_save_returns_latent_passthrough(tmp_path): assert result["result"][0] is latent +def test_png_save_latent_false_skips_sidecar_but_keeps_passthrough(tmp_path): + saver = FastAbsoluteSaver() + latent = {"samples": torch.ones((1, 1, 2, 2))} + + result = saver.save_images_fast(**_save_args(tmp_path, latent=latent, save_latent=False)) + + assert not (tmp_path / "frame_0000.latent").exists() + assert result["result"] == (latent,) + assert result["result"][0] is latent + + def test_video_save_writes_latent_sidecar_next_to_video(tmp_path): saver = FastAbsoluteSaver() latent = {"samples": torch.arange(8, dtype=torch.float32).reshape(2, 1, 2, 2)}