feat: skip model analysis for empty or non-model workflows

Add early-return optimizations so both backend and frontend avoid unnecessary
URL extraction and disk scanning when a workflow has no nodes or contains
no potential model references.

Also tighten API input validation by rejecting non-object workflow_json.
This commit is contained in:
Dariusz L
2026-06-24 03:46:50 +02:00
parent a01be11e27
commit 8c0168ab76
4 changed files with 178 additions and 2 deletions
+5 -1
View File
@@ -218,10 +218,14 @@ class ModelResolverExtension:
else str(force_rescan).lower() in {"1", "true", "yes"}
)
if not workflow_json:
if workflow_json is None:
return web.json_response(
{"error": "Workflow JSON is required"}, status=400
)
if not isinstance(workflow_json, dict):
return web.json_response(
{"error": "Workflow JSON must be an object"}, status=400
)
if analysis_id:
self.analysis_progress[analysis_id] = {
+145
View File
@@ -19,9 +19,12 @@ from .log_system.log_funcs import (
)
from .scanner import get_model_files
from .workflow_analyzer import (
NESTED_MODEL_KEYS,
NODE_TYPE_TO_CATEGORY_HINTS,
analyze_workflow_models,
get_model_widget_category_hint,
identify_missing_models,
should_scan_as_model_reference,
)
from .matcher import find_matches
from .workflow_updater import update_workflow_nodes
@@ -77,6 +80,127 @@ def get_workflow_url_info_for_filename(
return None
def workflow_has_nodes(workflow_json: Dict[str, Any]) -> bool:
"""Return True when the active top-level workflow contains nodes."""
if not isinstance(workflow_json, dict):
return False
nodes = workflow_json.get("nodes")
return isinstance(nodes, list) and len(nodes) > 0
def iter_active_workflow_nodes(workflow_json: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Return top-level nodes plus nodes from subgraphs referenced by top-level nodes."""
if not isinstance(workflow_json, dict):
return []
top_level_nodes = workflow_json.get("nodes")
if not isinstance(top_level_nodes, list):
return []
active_nodes = [
node for node in top_level_nodes if isinstance(node, dict)
]
definitions = workflow_json.get("definitions")
if not isinstance(definitions, dict):
return active_nodes
subgraph_list = definitions.get("subgraphs")
if not isinstance(subgraph_list, list):
return active_nodes
subgraphs = {
str(subgraph.get("id")): subgraph
for subgraph in subgraph_list
if isinstance(subgraph, dict) and subgraph.get("id") is not None
}
pending_subgraphs = [
str(node.get("type"))
for node in active_nodes
if str(node.get("type")) in subgraphs
]
seen_subgraphs = set()
while pending_subgraphs:
subgraph_id = pending_subgraphs.pop(0)
if subgraph_id in seen_subgraphs:
continue
seen_subgraphs.add(subgraph_id)
subgraph = subgraphs.get(subgraph_id)
subgraph_nodes = subgraph.get("nodes") if isinstance(subgraph, dict) else None
if not isinstance(subgraph_nodes, list):
continue
for node in subgraph_nodes:
if not isinstance(node, dict):
continue
active_nodes.append(node)
nested_subgraph_id = str(node.get("type"))
if (
nested_subgraph_id in subgraphs
and nested_subgraph_id not in seen_subgraphs
):
pending_subgraphs.append(nested_subgraph_id)
return active_nodes
def node_has_potential_model_reference(node: Dict[str, Any]) -> bool:
"""Detect model-looking widget values without resolving paths or scanning disks."""
if not isinstance(node, dict):
return False
widgets_values = node.get("widgets_values")
if not isinstance(widgets_values, list) or not widgets_values:
return False
node_type = node.get("type", "")
if (
node_type in {
"LoraLoaderV2",
"Lora Loader (LoraManager)",
"Lora Stacker (LoraManager)",
}
and len(widgets_values) >= 3
and isinstance(widgets_values[2], list)
):
for lora_item in widgets_values[2]:
if isinstance(lora_item, dict) and str(lora_item.get("name") or "").strip():
return True
for idx, value in enumerate(widgets_values):
model_widget_category_hint = get_model_widget_category_hint(node, idx)
if should_scan_as_model_reference(
value, declared_model_widget=bool(model_widget_category_hint)
):
return True
if not isinstance(value, dict):
continue
for nested_key in NESTED_MODEL_KEYS:
nested_value = value.get(nested_key)
if (
isinstance(nested_value, str)
and should_scan_as_model_reference(
nested_value, declared_model_widget=True
)
):
return True
return False
def workflow_has_potential_model_references(workflow_json: Dict[str, Any]) -> bool:
"""Return True when active workflow nodes contain any model-looking values."""
return any(
node_has_potential_model_reference(node)
for node in iter_active_workflow_nodes(workflow_json)
)
def normalize_workflow_download_url(url: str) -> str:
"""Convert workflow file-page URLs into direct download URLs when possible."""
if not isinstance(url, str) or not url:
@@ -392,6 +516,27 @@ def analyze_and_find_matches(
}
)
if (
not workflow_has_nodes(workflow_json)
or not workflow_has_potential_model_references(workflow_json)
):
if progress_callback:
progress_callback(
{
"stage": "completed",
"message": "Analysis complete",
"current": 0,
"total": 0,
}
)
return {
"missing_models": [],
"resolved_models": [],
"total_resolved": 0,
"total_missing": 0,
"total_models_analyzed": 0,
}
# Extract URLs from workflow (node.properties.models + regex)
workflow_urls = extract_workflow_urls(workflow_json)
log_debug(f"Extracted {len(workflow_urls)} URLs from workflow")
File diff suppressed because one or more lines are too long
@@ -164,6 +164,27 @@ export const lifecycleGraphMethods = {
return;
}
if (!this.workflowHasNodes(workflow)) {
this._analysisProgressToken = null;
const data = {
missing_models: [],
resolved_models: [],
total_missing: 0,
total_resolved: 0,
total_models_analyzed: 0
};
if (this._workflowDataLoadToken === loadToken) {
this.cachedWorkflowSignature = workflowSignature;
this.cachedAnalysisData = data;
this.saveAnalysisCacheForActiveWorkflow();
}
if (shouldRenderMissingModels()) {
await this.ensureDownloadDirectoriesLoaded();
this.displayMissingModels(this.contentElement, data);
}
return;
}
const analysisId = `an-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
this._analysisProgressToken = analysisId;
if (shouldRenderMissingModels()) {
@@ -218,6 +239,12 @@ export const lifecycleGraphMethods = {
}
},
workflowHasNodes(workflow) {
if (!workflow || typeof workflow !== 'object') return false;
return Array.isArray(workflow.nodes) && workflow.nodes.length > 0;
},
/**
* Get current workflow from ComfyUI
*/