fix(metadata-overwrite): use sentinel default for clip_skip to accept wired 0

This commit is contained in:
Will Miao
2026-07-28 23:13:00 +08:00
parent f92f958682
commit c9e5e784fc
6 changed files with 52 additions and 20 deletions

View File

@@ -1,5 +1,11 @@
"""Constants used by the metadata collector""" """Constants used by the metadata collector"""
# Sentinel value for clip_skip to distinguish "unconnected / widget default"
# from "user wired value 0". Both ComfyUI CLIPSetLastLayer (-24..-1) and
# A1111 conventions treat 0 as meaningless for clip skipping, but users may
# explicitly wire 0 to the overwrite node to express "no clip skip / default".
CLIP_SKIP_SENTINEL = -25
# Metadata categories # Metadata categories
MODELS = "models" MODELS = "models"
PROMPTS = "prompts" PROMPTS = "prompts"

View File

@@ -678,7 +678,12 @@ class MetadataProcessor:
for overwrite_info in metadata.get(OVERWRITE, {}).values(): for overwrite_info in metadata.get(OVERWRITE, {}).values():
overwrite_params = overwrite_info.get("parameters", {}) overwrite_params = overwrite_info.get("parameters", {})
for key, value in overwrite_params.items(): for key, value in overwrite_params.items():
if value: # truthy check — only overwrite when user provided a real value if key == "clip_skip":
# Accept any value from overwrite node (sentinel -25 already
# filtered upstream). Needed because falsy check treats 0
# as "not set" even though 0 is a valid wired input here.
params[key] = value
elif value: # truthy check — only overwrite when user provided a real value
params[key] = value params[key] = value
# Bridge: the overwrite node exposes the field as "model" (more accurate), # Bridge: the overwrite node exposes the field as "model" (more accurate),

View File

@@ -2,7 +2,7 @@ import json
import os import os
import re import re
from .constants import MODELS, PROMPTS, SAMPLING, LORAS, SIZE, IMAGES, IS_SAMPLER, OVERWRITE, METADATA_OVERWRITE_FIELDS from .constants import CLIP_SKIP_SENTINEL, MODELS, PROMPTS, SAMPLING, LORAS, SIZE, IMAGES, IS_SAMPLER, OVERWRITE, METADATA_OVERWRITE_FIELDS
def _store_checkpoint_metadata(metadata, node_id, model_name): def _store_checkpoint_metadata(metadata, node_id, model_name):
@@ -1236,7 +1236,10 @@ class MetadataOverwriteExtractor(NodeMetadataExtractor):
overwrite_params = {} overwrite_params = {}
for key in METADATA_OVERWRITE_FIELDS: for key in METADATA_OVERWRITE_FIELDS:
value = inputs.get(key) value = inputs.get(key)
if value: # truthy — only overwrite when user provided a real value if key == "clip_skip":
if value != CLIP_SKIP_SENTINEL:
overwrite_params[key] = value
elif value: # truthy — only overwrite when user provided a real value
overwrite_params[key] = value overwrite_params[key] = value
if overwrite_params: if overwrite_params:

View File

@@ -1,13 +1,18 @@
"""Metadata Overwrite node — allows users to manually specify generation parameters """Metadata Overwrite node — allows users to manually specify generation parameters
that override the automatically collected/inferred metadata. that override the automatically collected/inferred metadata.
All inputs have falsy defaults: only truthy (non-empty / non-zero) values Most inputs have falsy defaults (empty string / 0) which are skipped.
will overwrite the corresponding field in the final metadata. clip_skip uses a sentinel default (-25) so that a wired value of 0 is
preserved — both ComfyUI and A1111 conventions have no meaningful 0 value,
but users may wire 0 to express "no clip skip / default".
""" """
from typing import Any from typing import Any
from ..metadata_collector.constants import METADATA_OVERWRITE_FIELDS from ..metadata_collector.constants import (
CLIP_SKIP_SENTINEL as _CLIP_SKIP_SENTINEL,
METADATA_OVERWRITE_FIELDS,
)
class MetadataOverwriteLM: class MetadataOverwriteLM:
@@ -116,10 +121,14 @@ class MetadataOverwriteLM:
"clip_skip": ( "clip_skip": (
"INT", "INT",
{ {
"default": 0, "default": _CLIP_SKIP_SENTINEL,
"min": -24, "min": -25,
"max": 24, "max": 24,
"tooltip": "Clip skip. Only overwrites when non-zero.", "tooltip": (
"Clip skip (ComfyUI: -24..-1, A1111: 1+). "
"Default -25 means not set — any other value "
"overwrites."
),
}, },
), ),
"additional_data": ( "additional_data": (
@@ -144,14 +153,18 @@ class MetadataOverwriteLM:
OUTPUT_NODE = True OUTPUT_NODE = True
def collect_metadata(self, **kwargs: Any) -> tuple[dict[str, Any]]: def collect_metadata(self, **kwargs: Any) -> tuple[dict[str, Any]]:
"""Collect non-falsy input values into a metadata dict. """Collect non-default input values into a metadata dict.
Only values that are truthy (non-empty string, non-zero number) For most fields, a falsy value (empty string, 0) means "not set"
are included — matching the overwrite logic in the metadata pipeline. and is skipped. clip_skip uses a dedicated sentinel (-25) so that
a wired value of 0 is preserved and reaches the metadata pipeline.
""" """
result: dict[str, Any] = {} result: dict[str, Any] = {}
for key in METADATA_OVERWRITE_FIELDS: for key in METADATA_OVERWRITE_FIELDS:
value = kwargs.get(key) value = kwargs.get(key)
if value: if key == "clip_skip":
if value != _CLIP_SKIP_SENTINEL:
result[key] = value
elif value:
result[key] = value result[key] = value
return (result,) return (result,)

View File

@@ -476,11 +476,9 @@ class SaveImageLM:
params.append(f"Seed: {seed}") params.append(f"Seed: {seed}")
if size: if size:
params.append(f"Size: {size}") params.append(f"Size: {size}")
if clip_skip: if clip_skip is not None:
try: try:
cs = int(clip_skip) params.append(f"Clip skip: {abs(int(clip_skip))}")
if cs != 0:
params.append(f"Clip skip: {abs(cs)}")
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
additional_data = metadata_dict.get("additional_data", "") additional_data = metadata_dict.get("additional_data", "")

View File

@@ -870,7 +870,8 @@ def test_metadata_overwrite_extractor_stores_truthy_values(metadata_registry):
assert "steps" not in params assert "steps" not in params
assert "sampler" not in params assert "sampler" not in params
assert "scheduler" not in params assert "scheduler" not in params
assert "clip_skip" not in params # clip_skip=0 is now stored (0 != sentinel -25) — wired 0 is valid
assert params["clip_skip"] == 0
metadata_registry.clear_metadata() metadata_registry.clear_metadata()
@@ -880,8 +881,10 @@ def test_metadata_overwrite_extractor_empty_inputs(metadata_registry):
metadata_registry.start_collection("prompt-ow2") metadata_registry.start_collection("prompt-ow2")
metadata = metadata_registry.prompt_metadata["prompt-ow2"] metadata = metadata_registry.prompt_metadata["prompt-ow2"]
from py.metadata_collector.constants import CLIP_SKIP_SENTINEL
inputs = {key: "" for key in METADATA_OVERWRITE_FIELDS} inputs = {key: "" for key in METADATA_OVERWRITE_FIELDS}
inputs.update({"seed": 0, "steps": 0, "cfg_scale": 0.0, "clip_skip": 0}) inputs.update({"seed": 0, "steps": 0, "cfg_scale": 0.0, "clip_skip": CLIP_SKIP_SENTINEL})
MetadataOverwriteExtractor.extract("ow-2", inputs, None, metadata) MetadataOverwriteExtractor.extract("ow-2", inputs, None, metadata)
@@ -950,7 +953,8 @@ def test_extract_generation_params_overwrite_falsy_skipped(metadata_registry, po
registry_obj.set_current_prompt(populated_registry["prompt"]) registry_obj.set_current_prompt(populated_registry["prompt"])
metadata2 = registry_obj.prompt_metadata["promptA"] metadata2 = registry_obj.prompt_metadata["promptA"]
# Inject overwrite with falsy values # Inject overwrite with falsy values (except clip_skip=0 which is now
# treated as a valid wired input thanks to the -25 sentinel)
metadata2[OVERWRITE] = { metadata2[OVERWRITE] = {
"ow-1": { "ow-1": {
"parameters": { "parameters": {
@@ -974,6 +978,9 @@ def test_extract_generation_params_overwrite_falsy_skipped(metadata_registry, po
assert params["prompt"] == "A castle on a hill" assert params["prompt"] == "A castle on a hill"
assert params["cfg_scale"] == 7.5 assert params["cfg_scale"] == 7.5
# clip_skip=0 is a valid wired value (not the -25 sentinel) — should be applied
assert params["clip_skip"] == 0
registry_obj.clear_metadata() registry_obj.clear_metadata()