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:
digital-garbage
2026-08-27 04:41:38 +03:00
co-authored by Claude Opus 5
parent 138e13d901
commit 663eae2c3a
2 changed files with 205 additions and 22 deletions
+141 -15
View File
@@ -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")
+64 -7
View File
@@ -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