Refine workflow model reference detection

- Accept explicit model URNs without requiring model widget metadata
- Restrict model scanning to widgets with category/model hints or model-like filenames
- Remove over-broad single-widget-value detection path
- Add tests for cached model text, custom selectors, and constant/model slot edge cases
This commit is contained in:
Dariusz L
2026-07-19 19:10:54 +02:00
parent 8b2d200f77
commit fb600a3868
2 changed files with 147 additions and 13 deletions
+18 -13
View File
@@ -1012,17 +1012,11 @@ def get_model_widget_category_hints(
dynamic_category_hints = get_dynamic_widget_category_hints(node, widget_index)
output_category_hint = get_node_output_category_hint(node)
widgets_values = node.get("widgets_values", [])
has_single_widget_value = len(widgets_values) == 1
output_widget_category_hint = (
output_category_hint
if (
not dynamic_category_hints
and (
indexed_category_hint
or widget_category_hint
or has_single_widget_value
)
and (indexed_category_hint or widget_category_hint)
)
else None
)
@@ -1068,13 +1062,16 @@ def is_placeholder_model_value(value: Any) -> bool:
def should_scan_as_model_reference(value: Any, declared_model_widget: bool) -> bool:
"""Detect model references, including extensionless known model widgets."""
if is_model_filename(value):
"""Detect references only when the value or its widget provides model evidence."""
if isinstance(value, str) and URN_REGEX.match(value.strip()):
return True
if not declared_model_widget or not isinstance(value, str):
return False
if is_model_filename(value):
return True
return bool(value.strip()) and not is_placeholder_model_value(value)
@@ -1268,8 +1265,8 @@ def get_node_model_info(
"""
Extract model references from a single node.
This scans all widgets_values entries and tries to identify which ones
are model file references by attempting to resolve them.
This scans widget values backed by model metadata or a model-like widget name.
Explicit model URNs are also accepted without widget metadata.
Args:
node: Node dictionary from workflow JSON
@@ -1454,11 +1451,19 @@ def get_node_model_info(
if model_widget_category_hints
else ([effective_category_hint] if effective_category_hint else None)
)
category_backed_model_widget = bool(
model_widget_category_hint
or schema_output_category_hint
)
named_model_file_widget = bool(
is_workflow_model_widget_candidate(node, idx)
and is_model_filename(value)
)
if not should_scan_as_model_reference(
value,
declared_model_widget=bool(
model_widget_category_hint or schema_output_category_hint
declared_model_widget=(
category_backed_model_widget or named_model_file_widget
),
):
# Check for dict-type widget values containing model references (e.g. Power Lora Loader)
+129
View File
@@ -80,6 +80,135 @@ class WorkflowAnalyzerCaseSensitivityTests(unittest.TestCase):
class WorkflowAnalyzerCategoryHintTests(unittest.TestCase):
def test_show_anything_cached_model_text_is_not_a_model_reference(self):
with tempfile.TemporaryDirectory() as tmpdir:
model_dir = os.path.join(tmpdir, "KREA2")
os.makedirs(model_dir)
model_path = os.path.join(
model_dir, "krea2_turbo_fp8_scaled.safetensors"
)
with open(model_path, "wb"):
pass
workflow = {
"nodes": [
{
"id": 141,
"type": "easy showAnything",
"title": "Diffusion model",
"inputs": [{"name": "anything", "type": "*", "link": 700}],
"outputs": [{"name": "output", "type": "*", "links": []}],
"widgets": [{"name": "text"}],
"widgets_values": [
r"KREA2\krea2_turbo_fp8_scaled.safetensors"
],
}
]
}
available_models = [
{
"filename": "krea2_turbo_fp8_scaled.safetensors",
"path": model_path,
"relative_path": r"KREA2\krea2_turbo_fp8_scaled.safetensors",
"category": "diffusion_models",
"base_directory": tmpdir,
}
]
refs = analyze_workflow_models(
workflow, available_models=available_models
)
self.assertEqual([], refs)
def test_model_like_widget_name_keeps_custom_model_reference_detection(self):
workflow = {
"nodes": [
{
"id": 199,
"type": "CustomModelSelector",
"widgets": [{"name": "filename"}],
"widgets_values": ["custom_model.safetensors"],
"outputs": [],
}
]
}
refs = analyze_workflow_models(workflow, available_models=[])
self.assertEqual(1, len(refs))
self.assertEqual("custom_model.safetensors", refs[0]["original_path"])
def test_set_node_model_slot_name_does_not_turn_constant_into_model(self):
workflow = {
"nodes": [
{
"id": 3,
"type": "SetNode",
"title": "Set_MODEL_BASE",
"inputs": [{"name": "MODEL", "type": "MODEL", "link": 701}],
"outputs": [{"name": "MODEL", "type": "MODEL", "links": []}],
"widgets": [{"name": "Constant"}],
"widgets_values": ["MODEL_BASE"],
},
{
"id": 19,
"type": "SetNode",
"title": "Set_MODEL_LORA",
"inputs": [{"name": "MODEL", "type": "MODEL", "link": 702}],
"outputs": [{"name": "MODEL", "type": "MODEL", "links": []}],
"widgets": [{"name": "Constant"}],
"widgets_values": ["MODEL_LORA"],
},
]
}
refs = analyze_workflow_models(workflow, available_models=[])
self.assertEqual([], refs)
def test_model_slot_does_not_turn_device_widget_into_model(self):
workflow = {
"nodes": [
{
"id": 104,
"type": "CustomVideoUpscaler",
"inputs": [
{"name": "model", "type": "MODEL", "link": 703},
{
"name": "offload_device",
"type": "COMBO",
"widget": {"name": "offload_device"},
},
],
"outputs": [{"name": "IMAGE", "type": "IMAGE", "links": []}],
"widgets": [{"name": "offload_device"}],
"widgets_values": ["cuda:0"],
}
]
}
refs = analyze_workflow_models(workflow, available_models=[])
self.assertEqual([], refs)
def test_filename_widget_template_without_model_extension_is_ignored(self):
workflow = {
"nodes": [
{
"id": 155,
"type": "Image Saver",
"widgets": [{"name": "filename"}],
"widgets_values": ["Krea2_%time_%seed"],
"outputs": [],
}
]
}
refs = analyze_workflow_models(workflow, available_models=[])
self.assertEqual([], refs)
def test_upscale_model_loader_widget_named_model_name_stays_upscale(self):
workflow = {
"nodes": [