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 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 13:41:06 +02:00
co-authored by Claude Opus 4.8
parent 39997b1d34
commit 04be690a15
2 changed files with 23 additions and 5 deletions
+10 -4
View File
@@ -139,6 +139,10 @@ VIDEO_FORMATS = {
"nvenc_av1-mp4": {"ext": ".mp4", "codec": ["-c:v", "av1_nvenc"], "nvenc_av1-mp4": {"ext": ".mp4", "codec": ["-c:v", "av1_nvenc"],
"quality": "bitrate", "color_mgmt": True, "acodec": "aac", "quality": "bitrate", "color_mgmt": True, "acodec": "aac",
"extra": ["-movflags", "+faststart"]}, "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 --- # --- FORMAT SWITCH ---
"save_format": (["png", "webp", "mp4", "webm", "h265-mp4", "av1-mp4", "gif", "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 --- # --- NAMING CONTROL ---
"use_timestamp": ("BOOLEAN", {"default": False, "label": "Add Timestamp (Unique)"}), "use_timestamp": ("BOOLEAN", {"default": False, "label": "Add Timestamp (Unique)"}),
@@ -176,6 +181,7 @@ class FastAbsoluteSaver:
"metadata_key": ("STRING", {"default": "sharpness_score"}), "metadata_key": ("STRING", {"default": "sharpness_score"}),
"save_workflow_metadata": ("BOOLEAN", {"default": False, "label": "Save ComfyUI Workflow (Graph)"}), "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_metadata_png": ("BOOLEAN", {"default": False, "label": "Embed Workflow in PNG (sidecar or first file)"}),
"save_latent": ("BOOLEAN", {"default": True, "label": "Save Latent Sidecar"}),
# --- PERFORMANCE --- # --- PERFORMANCE ---
"max_threads": ("INT", {"default": 0, "min": 0, "max": 128, "step": 1, "label": "Max Threads (0=Auto)"}), "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 return out_file
def save_images_fast(self, images, output_path, filename_prefix, save_format, use_timestamp, auto_increment, counter_digits, 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, webp_lossless, webp_quality, webp_method,
video_fps, video_crf, video_pixel_format, video_fps, video_crf, video_pixel_format,
video_bitrate, prores_profile, gif_dither, video_bitrate, prores_profile, gif_dither,
@@ -538,7 +544,7 @@ class FastAbsoluteSaver:
extra_data=extra_pnginfo, extra_data=extra_pnginfo,
bitrate=video_bitrate, prores_profile=prores_profile, bitrate=video_bitrate, prores_profile=prores_profile,
gif_dither=gif_dither, audio=audio) 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) self._save_latent_sidecar(latent, out_file)
# Save metadata sidecar PNG next to the video file # Save metadata sidecar PNG next to the video file
if save_metadata_png: if save_metadata_png:
@@ -612,7 +618,7 @@ class FastAbsoluteSaver:
if future.result(): if future.result():
saved_image_paths.append(full_path) 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: for image_path in saved_image_paths:
self._save_latent_sidecar(latent, image_path) self._save_latent_sidecar(latent, image_path)
+13 -1
View File
@@ -4,7 +4,7 @@ import torch
from fast_saver import FastAbsoluteSaver 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 { return {
"images": torch.zeros((1, 2, 2, 3), dtype=torch.float32), "images": torch.zeros((1, 2, 2, 3), dtype=torch.float32),
"output_path": str(tmp_path), "output_path": str(tmp_path),
@@ -18,6 +18,7 @@ def _save_args(tmp_path, *, save_format="png", latent=None):
"metadata_key": "sharpness_score", "metadata_key": "sharpness_score",
"save_workflow_metadata": False, "save_workflow_metadata": False,
"save_metadata_png": False, "save_metadata_png": False,
"save_latent": save_latent,
"webp_lossless": True, "webp_lossless": True,
"webp_quality": 100, "webp_quality": 100,
"webp_method": 4, "webp_method": 4,
@@ -55,6 +56,17 @@ def test_png_save_returns_latent_passthrough(tmp_path):
assert result["result"][0] is latent 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): def test_video_save_writes_latent_sidecar_next_to_video(tmp_path):
saver = FastAbsoluteSaver() saver = FastAbsoluteSaver()
latent = {"samples": torch.arange(8, dtype=torch.float32).reshape(2, 1, 2, 2)} latent = {"samples": torch.arange(8, dtype=torch.float32).reshape(2, 1, 2, 2)}