feat(card): print what produced the video, not every widget that exists
Two rules: skip a value equal to the widget's own default, and skip a value whose feature is switched off. A region-sharpening prompt printed under a disabled sharpener read as if it had run, and enough of that buries the few values that did decide the render. On a realistic project this cuts the Chain Sampler section from 8 rows to 4. Defaults are read off the node's own INPUT_TYPES rather than copied here. Ownership mirrors the Editor's dependsOn wiring, and a test regenerates the table from engine_settings.js so the two cannot drift. Owner chains are walked to the end — v2a_grad_scale belongs to joyai_audio_memory, which belongs to joyai_memory. A section left with nothing to say is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cb5910568e
commit
138e13d901
@@ -148,6 +148,127 @@ def _pass_rows(cfg: dict) -> list:
|
||||
return rows
|
||||
|
||||
|
||||
# ── what is worth printing ───────────────────────────────────────────────────
|
||||
#
|
||||
# The card answers "what produced this video". A setting left at its default produced
|
||||
# nothing, and a setting belonging to a switched-off feature produced nothing either — a
|
||||
# region-sharpening prompt printed under a disabled sharpener reads as if it ran. Both are
|
||||
# noise, and enough of it buries the handful of values that DID decide the render.
|
||||
#
|
||||
# Two rules, both the user's: skip a value that equals the widget's own default, and skip a
|
||||
# value whose feature is off.
|
||||
|
||||
#: 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.
|
||||
_OWNED_BY = {
|
||||
"absolute_strength": ("steer_mode", None),
|
||||
"alg_anchor_sigma_threshold": ("alg_anchor", None),
|
||||
"alg_anchor_strength": ("alg_anchor", None),
|
||||
"alg_guide_blur_sigma_threshold": ("alg_blur_guides", None),
|
||||
"alg_guide_blur_strength": ("alg_blur_guides", None),
|
||||
"arcface_mode": ("identity_transfer_enabled", None),
|
||||
"context_window_freenoise": ("context_windows", None),
|
||||
"context_window_fuse": ("context_windows", None),
|
||||
"context_window_length": ("context_windows", None),
|
||||
"context_window_overlap": ("context_windows", None),
|
||||
"context_window_retain_first": ("context_windows", None),
|
||||
"context_window_schedule": ("context_windows", None),
|
||||
"debug_log": ("identity_transfer_enabled", None),
|
||||
"detail_max_area": ("segmented_detailing", None),
|
||||
"detail_mode": ("segmented_detailing", None),
|
||||
"detail_strength": ("segmented_detailing", None),
|
||||
"detail_targets": ("segmented_detailing", None),
|
||||
"detail_threshold": ("segmented_detailing", None),
|
||||
"dynashift_strength": ("dynashift", None),
|
||||
"dynashift_threshold": ("dynashift", None),
|
||||
"embed_guidance_source": ("embed_guidance", None),
|
||||
"embed_guidance_strength": ("embed_guidance", None),
|
||||
"h3_gain_audio": ("h3_gain_mode", "manual"),
|
||||
"h3_gain_prompt": ("h3_gain_mode", "manual"),
|
||||
"h3_gain_video": ("h3_gain_mode", "manual"),
|
||||
"h3_prompt_scale": ("h3_gain_mode", "manual"),
|
||||
"h3_taste_bias": ("h3_gain_mode", "manual"),
|
||||
"h3_video_detail": ("h3_gain_mode", "manual"),
|
||||
"id_strength": ("identity_transfer_enabled", None),
|
||||
"joyai_audio_memory": ("joyai_memory", None),
|
||||
"joyai_fix_frames": ("joyai_memory", None),
|
||||
"joyai_frame_select": ("joyai_memory", None),
|
||||
"joyai_memory_size": ("joyai_memory", None),
|
||||
"joyai_memory_strength": ("joyai_memory", None),
|
||||
"mid_scene_guide_strength": ("mid_scene_guide", None),
|
||||
"negative_erase_mode": ("negative_erase", None),
|
||||
"negative_erase_renorm": ("negative_erase", None),
|
||||
"negative_erase_strength": ("negative_erase", None),
|
||||
"output_guidance_strength": ("output_guidance", None),
|
||||
"phase_scale": ("identity_transfer_enabled", None),
|
||||
"plateau_cache_threshold": ("plateau_cache", None),
|
||||
"prompt_enhance_max_length": ("prompt_enhance", None),
|
||||
"prompt_enhance_system": ("prompt_enhance", None),
|
||||
"prompt_enhance_temperature": ("prompt_enhance", None),
|
||||
"prompt_enhance_thinking": ("prompt_enhance", None),
|
||||
"prompt_enhance_top_p": ("prompt_enhance", None),
|
||||
"score_slider_strength": ("score_slider", None),
|
||||
"source_id": ("identity_transfer_enabled", None),
|
||||
"v2a_grad_scale": ("joyai_audio_memory", None),
|
||||
}
|
||||
|
||||
|
||||
def _node_defaults(node_class: str) -> dict:
|
||||
"""Declared defaults for one node's widgets, read off the node itself.
|
||||
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
from . import bridge
|
||||
spec = bridge._funpack_attr("samplers" if "Sampler" in node_class else "conditioning",
|
||||
node_class).INPUT_TYPES()
|
||||
except Exception:
|
||||
return {}
|
||||
out = {}
|
||||
for group in ("required", "optional"):
|
||||
for name, decl in (spec.get(group) or {}).items():
|
||||
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"]
|
||||
elif isinstance(decl[0], (list, tuple)) and decl[0]:
|
||||
out[name] = decl[0][0] # a combo's first entry is its default
|
||||
return out
|
||||
|
||||
|
||||
def _switched_on(name: str, values: dict, defaults: dict, _seen=None) -> bool:
|
||||
"""Is the feature this knob belongs to actually running?
|
||||
|
||||
Walks the chain: v2a_grad_scale belongs to joyai_audio_memory, which belongs to
|
||||
joyai_memory. An owner that is itself off takes everything under it with it.
|
||||
"""
|
||||
owner, wanted = _OWNED_BY.get(name, (None, None))
|
||||
if owner is None:
|
||||
return True
|
||||
seen = _seen or set()
|
||||
if owner in seen: # a cycle in the table is a bug, not a hang
|
||||
return True
|
||||
seen.add(owner)
|
||||
current = values.get(owner, defaults.get(owner))
|
||||
live = bool(current) if wanted is None else str(current) == str(wanted)
|
||||
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)
|
||||
rows = []
|
||||
for name, value in (values or {}).items():
|
||||
if name in defaults and value == defaults[name]:
|
||||
continue
|
||||
if not _switched_on(name, values, defaults):
|
||||
continue
|
||||
rows.append((name, _short(value)))
|
||||
return rows
|
||||
|
||||
|
||||
def _sampling_sections(studio_inputs: dict, sampler_inputs: dict) -> list:
|
||||
"""Everything that decided HOW it was sampled, as card sections.
|
||||
|
||||
@@ -184,13 +305,13 @@ def _sampling_sections(studio_inputs: dict, sampler_inputs: dict) -> list:
|
||||
out.append({"title": "Studio", "node_class": "",
|
||||
"rows": [(k, _short(v)) for k, v in refiner.items()]})
|
||||
|
||||
extras = {k: v for k, v in studio_inputs.items() if k not in _SKIP_STUDIO_KEYS}
|
||||
extras = _live_rows({k: v for k, v in studio_inputs.items()
|
||||
if k not in _SKIP_STUDIO_KEYS}, "FunPackStudio")
|
||||
if extras:
|
||||
out.append({"title": "Studio inputs", "node_class": "",
|
||||
"rows": [(k, _short(v)) for k, v in extras.items()]})
|
||||
if sampler_inputs:
|
||||
out.append({"title": "Chain Sampler", "node_class": "",
|
||||
"rows": [(k, _short(v)) for k, v in sampler_inputs.items()]})
|
||||
out.append({"title": "Studio inputs", "node_class": "", "rows": extras})
|
||||
chain = _live_rows(sampler_inputs, "FunPackLTXAVSceneChainSampler")
|
||||
if chain:
|
||||
out.append({"title": "Chain Sampler", "node_class": "", "rows": chain})
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""The settings card prints what produced the video, not every widget that exists.
|
||||
|
||||
Reported from a rental: too much on the card to read, including a region-sharpening prompt
|
||||
printed under a sharpener that was switched off. A value left at its default produced
|
||||
nothing, and a value belonging to a disabled feature produced nothing either — but printed
|
||||
side by side with the handful that DID decide the render, they read as if they had.
|
||||
|
||||
Two rules: skip a value equal to the widget's own default, and skip a value whose feature
|
||||
is off.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from movie_editor.backend import settings_card as sc
|
||||
|
||||
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."""
|
||||
table = {
|
||||
"segmented_detailing": False, "detail_targets": "hands, face",
|
||||
"context_windows": False, "context_window_overlap": 40,
|
||||
"joyai_memory": False, "joyai_audio_memory": False, "v2a_grad_scale": 1.0,
|
||||
"h3_gain_mode": "learned", "h3_video_detail": 1.0,
|
||||
"cfg": 1.0, "frame_overlap": 8,
|
||||
}
|
||||
monkeypatch.setattr(sc, "_node_defaults", lambda _cls: table)
|
||||
return table
|
||||
|
||||
|
||||
def rows(values, defaults_unused=None):
|
||||
return dict(sc._live_rows(values, "FunPackLTXAVSceneChainSampler"))
|
||||
|
||||
|
||||
# ── rule 1: a default decided nothing ───────────────────────────────────────
|
||||
|
||||
def test_a_value_left_at_its_default_is_not_printed(defaults):
|
||||
assert rows({"cfg": 1.0}) == {}
|
||||
|
||||
|
||||
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."""
|
||||
assert "mystery" in rows({"mystery": 7})
|
||||
|
||||
|
||||
# ── rule 2: an off feature decided nothing ──────────────────────────────────
|
||||
|
||||
def test_a_setting_under_a_disabled_toggle_is_not_printed(defaults):
|
||||
"""The one that was reported: a region-sharpening prompt under a disabled sharpener."""
|
||||
assert rows({"detail_targets": "the hands, the face"}) == {}
|
||||
|
||||
|
||||
def test_the_same_setting_is_printed_once_its_feature_is_on(defaults):
|
||||
got = rows({"segmented_detailing": True, "detail_targets": "the hands, the face"})
|
||||
assert got["detail_targets"] == "the hands, the face"
|
||||
assert "segmented_detailing" in got
|
||||
|
||||
|
||||
def test_a_default_value_under_an_enabled_toggle_is_still_dropped(defaults):
|
||||
"""The two rules are independent: on does not make a default worth printing."""
|
||||
got = rows({"context_windows": True, "context_window_overlap": 40})
|
||||
assert "context_window_overlap" not in got
|
||||
|
||||
|
||||
def test_a_chain_of_owners_is_walked_to_the_end(defaults):
|
||||
"""v2a_grad_scale belongs to joyai_audio_memory, which belongs to joyai_memory. An
|
||||
owner that is itself off takes everything under it with it."""
|
||||
assert rows({"joyai_audio_memory": True, "v2a_grad_scale": 0.5}) == {}
|
||||
got = rows({"joyai_memory": True, "joyai_audio_memory": True, "v2a_grad_scale": 0.5})
|
||||
assert "v2a_grad_scale" in got
|
||||
|
||||
|
||||
def test_an_owner_matched_by_value_not_by_truthiness(defaults):
|
||||
"""h3_gain_mode is a combo: 'manual' switches its dials on, 'learned' does not — and
|
||||
'learned' is a perfectly truthy string."""
|
||||
assert rows({"h3_video_detail": 1.4}) == {}
|
||||
assert "h3_video_detail" in rows({"h3_gain_mode": "manual", "h3_video_detail": 1.4})
|
||||
|
||||
|
||||
def test_a_cycle_in_the_table_does_not_hang(monkeypatch, defaults):
|
||||
monkeypatch.setitem(sc._OWNED_BY, "a", ("b", None))
|
||||
monkeypatch.setitem(sc._OWNED_BY, "b", ("a", None))
|
||||
assert sc._live_rows({"a": 1, "b": 1}, "X") is not None
|
||||
|
||||
|
||||
# ── the table matches the Editor it was copied from ─────────────────────────
|
||||
|
||||
def test_the_ownership_table_is_the_editors_own():
|
||||
"""The Editor hides a knob whose feature is off; the card must hide the same ones. Two
|
||||
copies of one rule in two languages, so the test regenerates this one from that one."""
|
||||
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'dependsOn:\s*"([a-z0-9_]+)"(?:,\s*dependsValue:\s*"([^"]*)")?', body)
|
||||
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
|
||||
|
||||
|
||||
# ── the sections it feeds ───────────────────────────────────────────────────
|
||||
|
||||
def test_an_empty_section_is_dropped_rather_than_printed_bare(defaults):
|
||||
out = sc._sampling_sections({}, {"cfg": 1.0})
|
||||
assert not [s for s in out if s["title"] == "Chain Sampler"]
|
||||
|
||||
|
||||
def test_a_section_with_something_to_say_survives(defaults):
|
||||
out = sc._sampling_sections({}, {"cfg": 3.5})
|
||||
chain = next(s for s in out if s["title"] == "Chain Sampler")
|
||||
assert chain["rows"] == [("cfg", "3.5")]
|
||||
|
||||
|
||||
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)
|
||||
assert "INPUT_TYPES()" in src
|
||||
Reference in New Issue
Block a user