From b64636c1899413ff4fd4da63f7f906b062e5469e Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 5 Mar 2026 15:20:20 +0100 Subject: [PATCH] Add dynamic widget visibility and per-format pixel formats for FastAbsoluteSaver Hide/show format-specific widgets (CRF, bitrate, ProRes profile, GIF dither, pixel format, webp settings) based on selected save_format. Pixel format combo updates dynamically per codec. Remove hardcoded ffv1 pix_fmt to use widget value. Co-Authored-By: Claude Opus 4.6 --- fast_saver.py | 4 +- web/fast_saver.js | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 web/fast_saver.js diff --git a/fast_saver.py b/fast_saver.py index d93ece7..2731fb1 100644 --- a/fast_saver.py +++ b/fast_saver.py @@ -125,7 +125,7 @@ VIDEO_FORMATS = { "ffv1-mkv": {"ext": ".mkv", "codec": ["-c:v", "ffv1", "-level", "3", "-coder", "1", "-context", "1", "-g", "1", "-slices", "16", "-slicecrc", "1"], - "quality": "lossless", "pix_fmt": "yuv444p"}, + "quality": "lossless"}, "prores-mov": {"ext": ".mov", "codec": ["-c:v", "prores_ks"], "quality": "profile", "color_mgmt": True}, "nvenc_h264-mp4":{"ext": ".mp4", "codec": ["-c:v", "h264_nvenc"], @@ -175,7 +175,7 @@ class FastAbsoluteSaver: # --- VIDEO SPECIFIC --- "video_fps": ("INT", {"default": 24, "min": 1, "max": 120, "step": 1, "label": "Video FPS"}), "video_crf": ("INT", {"default": 18, "min": 0, "max": 51, "step": 1, "label": "Video CRF (0=Lossless, 51=Worst)"}), - "video_pixel_format": (["yuv420p", "yuv444p", "yuv420p10le"], {"label": "Pixel Format"}), + "video_pixel_format": (["yuv420p", "yuv422p", "yuv444p", "yuv420p10le", "rgb24", "bgra"], {"label": "Pixel Format"}), "video_bitrate": ("INT", {"default": 10, "min": 1, "max": 999, "step": 1, "label": "Video Bitrate (Mbps, NVENC)"}), "prores_profile": (["lt", "standard", "hq", "4444", "4444xq"], {"label": "ProRes Profile"}), "gif_dither": (["sierra2_4a", "floyd_steinberg", "bayer", "sierra2", "sierra3", "burkes", "atkinson", "heckbert", "none"], {"label": "GIF Dither Algorithm"}), diff --git a/web/fast_saver.js b/web/fast_saver.js new file mode 100644 index 0000000..2654113 --- /dev/null +++ b/web/fast_saver.js @@ -0,0 +1,109 @@ +import { app } from "../../scripts/app.js"; + +const FORMAT_WIDGETS = { + "png": [], + "webp": ["webp_lossless", "webp_quality", "webp_method"], + "mp4": ["video_fps", "video_crf", "video_pixel_format"], + "h265-mp4": ["video_fps", "video_crf", "video_pixel_format"], + "av1-mp4": ["video_fps", "video_crf", "video_pixel_format"], + "webm": ["video_fps", "video_crf", "video_pixel_format"], + "gif": ["video_fps", "gif_dither"], + "ffv1-mkv": ["video_fps", "video_pixel_format"], + "prores-mov": ["video_fps", "prores_profile"], + "nvenc_h264-mp4": ["video_fps", "video_pixel_format", "video_bitrate"], + "nvenc_hevc-mp4": ["video_fps", "video_pixel_format", "video_bitrate"], + "nvenc_av1-mp4": ["video_fps", "video_pixel_format", "video_bitrate"], +}; + +const FORMAT_PIX_FMTS = { + "mp4": ["yuv420p", "yuv444p"], + "h265-mp4": ["yuv420p", "yuv444p", "yuv420p10le"], + "av1-mp4": ["yuv420p", "yuv420p10le"], + "webm": ["yuv420p", "yuv444p", "yuv420p10le"], + "ffv1-mkv": ["yuv420p", "yuv422p", "yuv444p", "rgb24", "bgra"], + "nvenc_h264-mp4": ["yuv420p", "yuv444p"], + "nvenc_hevc-mp4": ["yuv420p", "yuv444p", "yuv420p10le"], + "nvenc_av1-mp4": ["yuv420p", "yuv420p10le"], +}; + +const ALL_MANAGED = [ + "webp_lossless", "webp_quality", "webp_method", + "video_fps", "video_crf", "video_pixel_format", + "video_bitrate", "prores_profile", "gif_dither", +]; + +function hideWidget(node, widget) { + if (widget.origType === undefined) widget.origType = widget.type; + widget.type = "hidden"; + widget.computeSize = () => [0, -4]; +} + +function showWidget(node, widget) { + if (widget.origType !== undefined) { + widget.type = widget.origType; + delete widget.origType; + delete widget.computeSize; + } +} + +function updateVisibility(node) { + const formatWidget = node.widgets?.find(w => w.name === "save_format"); + if (!formatWidget) return; + + const format = formatWidget.value; + const visible = new Set(FORMAT_WIDGETS[format] || []); + + for (const name of ALL_MANAGED) { + const w = node.widgets?.find(w => w.name === name); + if (!w) continue; + if (visible.has(name)) { + showWidget(node, w); + } else { + hideWidget(node, w); + } + } + + // Update pixel format combo options + const pixWidget = node.widgets?.find(w => w.name === "video_pixel_format"); + if (pixWidget && FORMAT_PIX_FMTS[format]) { + const opts = FORMAT_PIX_FMTS[format]; + pixWidget.options.values = opts; + if (!opts.includes(pixWidget.value)) { + pixWidget.value = opts[0]; + } + } + + node.setSize(node.computeSize()); + app.graph?.setDirtyCanvas(true, true); +} + +app.registerExtension({ + name: "fast.absolute.saver.visibility", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "FastAbsoluteSaver") return; + + const origOnNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + origOnNodeCreated?.apply(this, arguments); + + const formatWidget = this.widgets?.find(w => w.name === "save_format"); + if (formatWidget) { + const origCallback = formatWidget.callback; + formatWidget.callback = (...args) => { + origCallback?.apply(formatWidget, args); + updateVisibility(this); + }; + } + + // Defer initial update so all widgets exist + queueMicrotask(() => updateVisibility(this)); + }; + + const origOnConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function (info) { + origOnConfigure?.apply(this, arguments); + queueMicrotask(() => updateVisibility(this)); + }; + }, +});