feat(card): drop dead keys and defaults from the Studio block too
On the card from the rental: Chain Sampler 35 rows -> 4, Studio 13 -> 6, Studio inputs 2 -> 0. Three additions. A key the node does not DECLARE is now dropped rather than kept: ComfyUI builds a node's arguments from its own INPUT_TYPES, so `block_steer` (removed from this pack entirely) and a `steps` key the Chain Sampler has never had reached nothing, while printing beside live settings as if they had. An empty value decided nothing either. And the studio_settings refiner block is filtered against the Editor's own defaults — it has no node to ask, so an unrecognised key there is still kept: unjudgeable is not inert. identity_projector was printing a model filename under a disabled feature. It is rendered by hand in the Editor so it carries no dependsOn, hence _OWNED_EXTRA, kept apart so the generated half stays generated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
138e13d901
commit
663eae2c3a
@@ -161,7 +161,14 @@ def _pass_rows(cfg: dict) -> list:
|
||||
#: knob -> (the toggle that owns it, the value of that toggle which switches it on).
|
||||
#: None means "any truthy value". Mirrored from the Editor's own dependsOn wiring, and a
|
||||
#: test regenerates this from engine_settings.js so the two cannot drift apart.
|
||||
#: knobs the Editor renders by hand rather than from its knob table, so they carry no
|
||||
#: dependsOn for the test below to find. Kept separate so the generated half stays generated.
|
||||
_OWNED_EXTRA = {
|
||||
"identity_projector": ("identity_transfer_enabled", None),
|
||||
}
|
||||
|
||||
_OWNED_BY = {
|
||||
**_OWNED_EXTRA,
|
||||
"absolute_strength": ("steer_mode", None),
|
||||
"alg_anchor_sigma_threshold": ("alg_anchor", None),
|
||||
"alg_anchor_strength": ("alg_anchor", None),
|
||||
@@ -214,28 +221,125 @@ _OWNED_BY = {
|
||||
}
|
||||
|
||||
|
||||
def _node_defaults(node_class: str) -> dict:
|
||||
"""Declared defaults for one node's widgets, read off the node itself.
|
||||
#: every knob the Editor offers, and the value it ships with. Used for the settings that
|
||||
#: live inside the studio_settings blob rather than on a node, where there is no INPUT_TYPES
|
||||
#: to ask. Regenerated from engine_settings.js by a test, like the table above.
|
||||
_EDITOR_DEFAULTS = {
|
||||
"absolute_strength": 0.6,
|
||||
"alg_anchor": False,
|
||||
"alg_anchor_sigma_threshold": 0.975,
|
||||
"alg_anchor_strength": 2.0,
|
||||
"alg_blur_guides": False,
|
||||
"alg_guide_blur_sigma_threshold": 0.975,
|
||||
"alg_guide_blur_strength": 2.0,
|
||||
"arcface_mode": "auto_adjust",
|
||||
"bounded_attention_enabled": False,
|
||||
"carry_i2v_guides": False,
|
||||
"carry_overlap_through_anchor": False,
|
||||
"cfg": 1.0,
|
||||
"context_window_freenoise": True,
|
||||
"context_window_fuse": "pyramid",
|
||||
"context_window_length": 145,
|
||||
"context_window_overlap": 40,
|
||||
"context_window_retain_first": False,
|
||||
"context_window_schedule": "standard_uniform",
|
||||
"context_windows": False,
|
||||
"cut_opening_frames": 0,
|
||||
"debug_log": False,
|
||||
"decode_noise_scale": 0.0,
|
||||
"decode_tile_size": 0,
|
||||
"decode_timestep": 0.05,
|
||||
"detail_denoise": 0.85,
|
||||
"detail_max_area": 0.35,
|
||||
"detail_mode": "repair",
|
||||
"detail_strength": 1.0,
|
||||
"detail_targets": "hands",
|
||||
"detail_threshold": 0.35,
|
||||
"dynashift": False,
|
||||
"dynashift_strength": 0.3,
|
||||
"dynashift_threshold": 0.6,
|
||||
"embed_guidance": False,
|
||||
"embed_guidance_source": "relative",
|
||||
"embed_guidance_strength": 0.02,
|
||||
"frame_overlap": 16,
|
||||
"h3_audio_clock": False,
|
||||
"h3_gain_audio": 1.0,
|
||||
"h3_gain_mode": "learned",
|
||||
"h3_gain_prompt": 1.0,
|
||||
"h3_gain_video": 1.0,
|
||||
"h3_phrase_emphasis": False,
|
||||
"h3_prompt_scale": 1.0,
|
||||
"h3_prompt_time": 0.0,
|
||||
"h3_render_gains": True,
|
||||
"h3_taste_bias": 0.0,
|
||||
"h3_video_detail": 1.0,
|
||||
"id_strength": 1.0,
|
||||
"identity_transfer_enabled": False,
|
||||
"joyai_audio_memory": False,
|
||||
"joyai_fix_frames": 3,
|
||||
"joyai_frame_select": "center",
|
||||
"joyai_memory": False,
|
||||
"joyai_memory_size": 7,
|
||||
"joyai_memory_strength": 0.3,
|
||||
"mid_scene_guide": False,
|
||||
"mid_scene_guide_strength": 0.25,
|
||||
"negative_erase": False,
|
||||
"negative_erase_mode": "project",
|
||||
"negative_erase_renorm": True,
|
||||
"negative_erase_strength": 0.5,
|
||||
"output_guidance": False,
|
||||
"output_guidance_strength": 0.02,
|
||||
"phase_scale": 1.0,
|
||||
"plateau_cache": False,
|
||||
"plateau_cache_threshold": 0.975,
|
||||
"prompt_enhance": False,
|
||||
"prompt_enhance_max_length": 400,
|
||||
"prompt_enhance_system": "",
|
||||
"prompt_enhance_temperature": 0.7,
|
||||
"prompt_enhance_thinking": False,
|
||||
"prompt_enhance_top_p": 0.92,
|
||||
"reference_injection": False,
|
||||
"score_slider": False,
|
||||
"score_slider_strength": 1.0,
|
||||
"second_pass_op": "none",
|
||||
"second_pass_upscale": 2.0,
|
||||
"segmented_detailing": False,
|
||||
"source_id": 2.0,
|
||||
"split_transition_placement": "start",
|
||||
"steer_mode": "relative",
|
||||
"taste_nearest_prompt": False,
|
||||
"temporal_style": "natural",
|
||||
"transition_duration": 16,
|
||||
"use_same_seed": False,
|
||||
"v2a_grad_scale": 1.0,
|
||||
"value_guidance": True,
|
||||
"vision_conditioning": True,
|
||||
}
|
||||
|
||||
Off the NODE rather than a table here: the node is the thing whose value is being
|
||||
printed, so a default copied to this file would be one more thing to keep in step.
|
||||
|
||||
def _node_inputs(node_class: str) -> tuple:
|
||||
"""(every input this node declares, the ones that have a default).
|
||||
|
||||
Read off the NODE rather than copied here: it is the thing whose values are being
|
||||
printed, so a table in this file would be one more thing to keep in step.
|
||||
"""
|
||||
try:
|
||||
from . import bridge
|
||||
spec = bridge._funpack_attr("samplers" if "Sampler" in node_class else "conditioning",
|
||||
node_class).INPUT_TYPES()
|
||||
except Exception:
|
||||
return {}
|
||||
out = {}
|
||||
return frozenset(), {}
|
||||
declared, defaults = set(), {}
|
||||
for group in ("required", "optional"):
|
||||
for name, decl in (spec.get(group) or {}).items():
|
||||
declared.add(name)
|
||||
if not isinstance(decl, (list, tuple)) or not decl:
|
||||
continue
|
||||
if len(decl) >= 2 and isinstance(decl[1], dict) and "default" in decl[1]:
|
||||
out[name] = decl[1]["default"]
|
||||
defaults[name] = decl[1]["default"]
|
||||
elif isinstance(decl[0], (list, tuple)) and decl[0]:
|
||||
out[name] = decl[0][0] # a combo's first entry is its default
|
||||
return out
|
||||
defaults[name] = decl[0][0] # a combo's first entry is its default
|
||||
return frozenset(declared), defaults
|
||||
|
||||
|
||||
def _switched_on(name: str, values: dict, defaults: dict, _seen=None) -> bool:
|
||||
@@ -256,13 +360,33 @@ def _switched_on(name: str, values: dict, defaults: dict, _seen=None) -> bool:
|
||||
return live and _switched_on(owner, values, defaults, seen)
|
||||
|
||||
|
||||
def _live_rows(values: dict, node_class: str) -> list:
|
||||
"""`values` filtered down to what actually decided this render."""
|
||||
defaults = _node_defaults(node_class)
|
||||
def _empty(value) -> bool:
|
||||
return value is None or (isinstance(value, (str, list, dict, tuple)) and len(value) == 0)
|
||||
|
||||
|
||||
def _live_rows(values: dict, node_class: str = "", defaults: dict = None) -> list:
|
||||
"""`values` filtered down to what actually decided this render.
|
||||
|
||||
With a `node_class`, a key the node does not declare is DROPPED: ComfyUI builds a node's
|
||||
arguments from its own INPUT_TYPES, so a leftover key from a feature that no longer
|
||||
exists reaches nothing. `block_steer` was removed from this pack entirely and was still
|
||||
being printed as a live setting; so was a `steps` key the Chain Sampler has never had.
|
||||
"""
|
||||
declared = frozenset()
|
||||
if node_class:
|
||||
declared, node_defaults = _node_inputs(node_class)
|
||||
defaults = node_defaults if defaults is None else {**defaults, **node_defaults}
|
||||
defaults = defaults or {}
|
||||
rows = []
|
||||
for name, value in (values or {}).items():
|
||||
if str(name).startswith("_"): # the builder's own private channel
|
||||
continue
|
||||
if declared and name not in declared: # cannot reach the render at all
|
||||
continue
|
||||
if name in defaults and value == defaults[name]:
|
||||
continue
|
||||
if _empty(value): # decided nothing either
|
||||
continue
|
||||
if not _switched_on(name, values, defaults):
|
||||
continue
|
||||
rows.append((name, _short(value)))
|
||||
@@ -301,9 +425,11 @@ def _sampling_sections(studio_inputs: dict, sampler_inputs: dict) -> list:
|
||||
out.append({"title": "Second pass", "node_class": "", "rows": rows})
|
||||
|
||||
refiner = settings.get("refiner") if isinstance(settings.get("refiner"), dict) else {}
|
||||
if refiner:
|
||||
out.append({"title": "Studio", "node_class": "",
|
||||
"rows": [(k, _short(v)) for k, v in refiner.items()]})
|
||||
# No node to ask here — the refiner block lives inside the studio_settings blob — so an
|
||||
# unrecognised key is KEPT. Unjudgeable is not the same as inert.
|
||||
refiner_rows = _live_rows(refiner, defaults=_EDITOR_DEFAULTS)
|
||||
if refiner_rows:
|
||||
out.append({"title": "Studio", "node_class": "", "rows": refiner_rows})
|
||||
|
||||
extras = _live_rows({k: v for k, v in studio_inputs.items()
|
||||
if k not in _SKIP_STUDIO_KEYS}, "FunPackStudio")
|
||||
|
||||
@@ -23,7 +23,7 @@ FRONTEND = Path(__file__).resolve().parents[1] / "movie_editor" / "frontend"
|
||||
|
||||
@pytest.fixture
|
||||
def defaults(monkeypatch):
|
||||
"""Stand in for the node's declared defaults, so these tests describe the RULE."""
|
||||
"""Stand in for the node's declared inputs, so these tests describe the RULE."""
|
||||
table = {
|
||||
"segmented_detailing": False, "detail_targets": "hands, face",
|
||||
"context_windows": False, "context_window_overlap": 40,
|
||||
@@ -31,7 +31,8 @@ def defaults(monkeypatch):
|
||||
"h3_gain_mode": "learned", "h3_video_detail": 1.0,
|
||||
"cfg": 1.0, "frame_overlap": 8,
|
||||
}
|
||||
monkeypatch.setattr(sc, "_node_defaults", lambda _cls: table)
|
||||
declared = frozenset(table) | {"mystery", "adjustments"}
|
||||
monkeypatch.setattr(sc, "_node_inputs", lambda _cls: (declared, table))
|
||||
return table
|
||||
|
||||
|
||||
@@ -49,12 +50,51 @@ def test_a_changed_value_is_printed(defaults):
|
||||
assert "cfg" in rows({"cfg": 3.5})
|
||||
|
||||
|
||||
def test_a_value_the_node_does_not_declare_is_kept(defaults):
|
||||
"""Not every key on the card comes from a widget. Unknown means unjudgeable, and
|
||||
dropping it would hide something the card was asked to show."""
|
||||
def test_a_declared_key_with_no_default_is_kept(defaults):
|
||||
assert "mystery" in rows({"mystery": 7})
|
||||
|
||||
|
||||
def test_a_key_the_node_does_not_declare_is_dropped(defaults):
|
||||
"""Reported from a rental: the card showed `block_steer` and a `steps` value. Neither is
|
||||
an input on the Chain Sampler — block_steer was removed from the pack entirely — so
|
||||
ComfyUI never passes them to anything. Printed beside live settings they read as live."""
|
||||
assert rows({"block_steer": True, "steps": 40}) == {}
|
||||
|
||||
|
||||
def test_an_empty_value_decided_nothing_either(defaults):
|
||||
assert rows({"adjustments": [], "mystery": ""}) == {}
|
||||
|
||||
|
||||
def test_the_builders_private_keys_are_not_settings(defaults):
|
||||
assert rows({"_movie_editor_scene_ratings": {"a": 1}}) == {}
|
||||
|
||||
|
||||
# ── the studio_settings blob, which has no node to ask ──────────────────────
|
||||
|
||||
def test_a_refiner_value_at_its_editor_default_is_dropped():
|
||||
assert sc._live_rows({"temporal_style": "natural", "steer_mode": "relative"},
|
||||
defaults=sc._EDITOR_DEFAULTS) == []
|
||||
|
||||
|
||||
def test_a_changed_refiner_value_survives():
|
||||
got = dict(sc._live_rows({"temporal_style": "slow_motion"},
|
||||
defaults=sc._EDITOR_DEFAULTS))
|
||||
assert got["temporal_style"] == "slow_motion"
|
||||
|
||||
|
||||
def test_an_unrecognised_refiner_key_is_kept():
|
||||
"""No node declares these, so unknown means unjudgeable — not inert. Dropping it would
|
||||
hide something rather than tidy it."""
|
||||
got = dict(sc._live_rows({"prompt_repair": True}, defaults=sc._EDITOR_DEFAULTS))
|
||||
assert "prompt_repair" in got
|
||||
|
||||
|
||||
def test_a_hand_built_control_still_gets_an_owner():
|
||||
"""identity_projector is rendered by hand in the Editor, so it carries no dependsOn for
|
||||
the generator to find — and it was printing a model filename under a disabled feature."""
|
||||
assert sc._OWNED_BY["identity_projector"] == ("identity_transfer_enabled", None)
|
||||
|
||||
|
||||
# ── rule 2: an off feature decided nothing ──────────────────────────────────
|
||||
|
||||
def test_a_setting_under_a_disabled_toggle_is_not_printed(defaults):
|
||||
@@ -108,7 +148,24 @@ def test_the_ownership_table_is_the_editors_own():
|
||||
if m:
|
||||
found[name] = (m.group(1), m.group(2))
|
||||
assert found, "no dependsOn rows parsed — the parser, not the table, is what broke"
|
||||
assert found == sc._OWNED_BY
|
||||
assert {**found, **sc._OWNED_EXTRA} == sc._OWNED_BY
|
||||
|
||||
|
||||
def test_the_defaults_table_is_the_editors_own():
|
||||
src = (FRONTEND / "engine_settings.js").read_text(encoding="utf-8")
|
||||
found = {}
|
||||
for name, body in re.findall(r'\{\s*name:\s*"([a-z0-9_.]+)"(.*?)\n\s*(?=\{ name:|\];)',
|
||||
src, re.S):
|
||||
m = re.search(r'default:\s*(true|false|-?[\d.]+|"[^"]*")', body)
|
||||
if m:
|
||||
raw = m.group(1)
|
||||
found[name] = {"true": True, "false": False}.get(raw) if raw in ("true", "false") \
|
||||
else (raw.strip('"') if raw.startswith('"') else float(raw))
|
||||
assert found, "no default rows parsed"
|
||||
for name, value in found.items():
|
||||
assert name in sc._EDITOR_DEFAULTS, name
|
||||
assert float(sc._EDITOR_DEFAULTS[name]) == value if isinstance(value, float) \
|
||||
else sc._EDITOR_DEFAULTS[name] == value, name
|
||||
|
||||
|
||||
# ── the sections it feeds ───────────────────────────────────────────────────
|
||||
@@ -127,5 +184,5 @@ def test_a_section_with_something_to_say_survives(defaults):
|
||||
def test_defaults_are_read_off_the_node_not_copied_here():
|
||||
"""A default copied into this file is one more thing to keep in step with the node."""
|
||||
import inspect
|
||||
src = inspect.getsource(sc._node_defaults)
|
||||
src = inspect.getsource(sc._node_inputs)
|
||||
assert "INPUT_TYPES()" in src
|
||||
|
||||
Reference in New Issue
Block a user