workflow_analyzer: handle implicit control_after_generate widgets in dynamic layouts
Treat control_after_generate widgets that are implicitly inserted after seed widgets as non-model widgets when analyzing dynamic node layouts. This prevents the resolver from attempting to resolve control values such as 'fixed', 'randomize', 'increment', and 'decrement' as model paths.
This commit is contained in:
+96
-15
@@ -862,29 +862,110 @@ def get_dynamic_node_widget_category_hints(node_type: str) -> Dict[str, Any]:
|
||||
return hints
|
||||
|
||||
|
||||
CONTROL_AFTER_GENERATE_VALUES = {
|
||||
"fixed",
|
||||
"increment",
|
||||
"decrement",
|
||||
"randomize",
|
||||
}
|
||||
|
||||
|
||||
def _normalized_index_mapping(value: Any) -> Dict[int, str]:
|
||||
if not isinstance(value, dict):
|
||||
return {}
|
||||
|
||||
normalized: Dict[int, str] = {}
|
||||
for raw_index, raw_name in value.items():
|
||||
try:
|
||||
index = int(raw_index)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if index < 0:
|
||||
continue
|
||||
normalized[index] = str(raw_name or "").strip()
|
||||
return normalized
|
||||
|
||||
|
||||
def _dynamic_serialized_widget_layout(
|
||||
node: Dict[str, Any], hints: Dict[str, Any]
|
||||
) -> tuple[Dict[int, str], Set[int]]:
|
||||
names_by_index = _normalized_index_mapping(
|
||||
hints.get("serialized_name_by_index", {})
|
||||
)
|
||||
non_model_indices = {
|
||||
int(index)
|
||||
for index in hints.get("non_model_by_index", [])
|
||||
if str(index).isdigit()
|
||||
}
|
||||
if hints.get("has_generated_widgets"):
|
||||
return names_by_index, non_model_indices
|
||||
|
||||
widgets_values = node.get("widgets_values", [])
|
||||
if not isinstance(widgets_values, list) or not names_by_index:
|
||||
return {}, set()
|
||||
|
||||
expected_widget_count = max(names_by_index) + 1
|
||||
remaining_extra_widgets = len(widgets_values) - expected_widget_count
|
||||
if remaining_extra_widgets <= 0:
|
||||
return {}, set()
|
||||
|
||||
effective_names: Dict[int, str] = {}
|
||||
effective_non_model_indices: Set[int] = set()
|
||||
inserted_widget_count = 0
|
||||
|
||||
for base_index in sorted(names_by_index):
|
||||
widget_name = names_by_index[base_index]
|
||||
actual_index = base_index + inserted_widget_count
|
||||
effective_names[actual_index] = widget_name
|
||||
|
||||
normalized_name = normalize_widget_name(widget_name)
|
||||
control_index = actual_index + 1
|
||||
seed_value = (
|
||||
widgets_values[actual_index]
|
||||
if actual_index < len(widgets_values)
|
||||
else None
|
||||
)
|
||||
control_value = (
|
||||
str(widgets_values[control_index] or "").strip().lower()
|
||||
if control_index < len(widgets_values)
|
||||
else ""
|
||||
)
|
||||
is_seed_widget = normalized_name == "seed" or normalized_name.endswith(
|
||||
"_seed"
|
||||
)
|
||||
if (
|
||||
remaining_extra_widgets > 0
|
||||
and is_seed_widget
|
||||
and isinstance(seed_value, int)
|
||||
and not isinstance(seed_value, bool)
|
||||
and control_value in CONTROL_AFTER_GENERATE_VALUES
|
||||
):
|
||||
effective_names[control_index] = "control_after_generate"
|
||||
effective_non_model_indices.add(control_index)
|
||||
inserted_widget_count += 1
|
||||
remaining_extra_widgets -= 1
|
||||
|
||||
if not effective_non_model_indices:
|
||||
return {}, set()
|
||||
return effective_names, effective_non_model_indices
|
||||
|
||||
|
||||
def get_dynamic_serialized_widget_name(
|
||||
node: Dict[str, Any], widget_index: int
|
||||
) -> str:
|
||||
hints = get_dynamic_node_widget_category_hints(str(node.get("type", "") or ""))
|
||||
if not hints.get("has_generated_widgets"):
|
||||
return ""
|
||||
|
||||
names_by_index = hints.get("serialized_name_by_index", {})
|
||||
if not isinstance(names_by_index, dict):
|
||||
return ""
|
||||
|
||||
return str(
|
||||
names_by_index.get(widget_index, names_by_index.get(str(widget_index), "")) or ""
|
||||
).strip()
|
||||
names_by_index, _non_model_indices = _dynamic_serialized_widget_layout(
|
||||
node, hints
|
||||
)
|
||||
return names_by_index.get(widget_index, "")
|
||||
|
||||
|
||||
def is_dynamic_non_model_widget(node: Dict[str, Any], widget_index: int) -> bool:
|
||||
hints = get_dynamic_node_widget_category_hints(str(node.get("type", "") or ""))
|
||||
if not hints.get("has_generated_widgets"):
|
||||
return False
|
||||
|
||||
non_model_indices = hints.get("non_model_by_index", [])
|
||||
return widget_index in non_model_indices or str(widget_index) in non_model_indices
|
||||
_names_by_index, non_model_indices = _dynamic_serialized_widget_layout(
|
||||
node, hints
|
||||
)
|
||||
return widget_index in non_model_indices
|
||||
|
||||
|
||||
def get_dynamic_widget_category_hints(
|
||||
|
||||
@@ -326,6 +326,77 @@ class WorkflowAnalyzerCategoryHintTests(unittest.TestCase):
|
||||
self.assertEqual(r"LTXV\model.safetensors", refs[0]["original_path"])
|
||||
self.assertEqual("checkpoints", refs[0]["category"])
|
||||
|
||||
def test_implicit_seed_control_keeps_following_model_aligned(self):
|
||||
dynamic_hints = {
|
||||
"widget_names": ["noise_seed", "clip_name", "strength"],
|
||||
"by_name": {"clip_name": ["clip_vision"]},
|
||||
"by_index": {1: ["clip_vision"]},
|
||||
"serialized_name_by_index": {
|
||||
0: "noise_seed",
|
||||
1: "clip_name",
|
||||
2: "strength",
|
||||
},
|
||||
"non_model_by_index": [],
|
||||
"has_generated_widgets": False,
|
||||
"choice_info_by_name": {
|
||||
"clip_name": {
|
||||
"source": "folder_paths",
|
||||
"choices": [],
|
||||
}
|
||||
},
|
||||
"choice_info_by_index": {
|
||||
1: {"source": "folder_paths", "choices": []}
|
||||
},
|
||||
}
|
||||
workflow = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": 321,
|
||||
"type": "UltraSharkSampler Tiled",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "noise_seed",
|
||||
"type": "INT",
|
||||
"widget": {"name": "noise_seed"},
|
||||
},
|
||||
{
|
||||
"name": "clip_name",
|
||||
"type": "COMBO",
|
||||
"widget": {"name": "clip_name"},
|
||||
},
|
||||
{
|
||||
"name": "strength",
|
||||
"type": "FLOAT",
|
||||
"widget": {"name": "strength"},
|
||||
},
|
||||
],
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
"clip-vit-large-patch14.safetensors",
|
||||
1.0,
|
||||
],
|
||||
"outputs": [{"type": "LATENT", "links": []}],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with patch(
|
||||
"core.workflow_analyzer.get_dynamic_node_widget_category_hints",
|
||||
return_value=dynamic_hints,
|
||||
):
|
||||
refs = analyze_workflow_models(workflow, available_models=[])
|
||||
|
||||
self.assertEqual(1, len(refs))
|
||||
self.assertEqual(2, refs[0]["widget_index"])
|
||||
self.assertEqual("clip_name", refs[0]["widget_name"])
|
||||
self.assertEqual(
|
||||
"clip-vit-large-patch14.safetensors",
|
||||
refs[0]["original_path"],
|
||||
)
|
||||
self.assertEqual("clip_vision", refs[0]["category"])
|
||||
self.assertFalse(refs[0]["exists"])
|
||||
|
||||
def test_non_model_static_choices_in_hybrid_model_dropdowns_are_skipped(self):
|
||||
cases = [
|
||||
("DependenciesEdit", "ckpt_name", "checkpoints", "Original"),
|
||||
|
||||
Reference in New Issue
Block a user