Improve LoRA model strength detection across workflow analysis and UI
This commit is contained in:
+7
-15
@@ -1676,6 +1676,7 @@ class ModelResolverExtension:
|
||||
from .core.workflow_analyzer import (
|
||||
URN_TYPE_MAP,
|
||||
analyze_workflow_models,
|
||||
get_lora_model_strength,
|
||||
)
|
||||
|
||||
update_loaded_progress(
|
||||
@@ -1887,25 +1888,16 @@ class ModelResolverExtension:
|
||||
model_name = get_filename_from_path(original_path)
|
||||
strength = None
|
||||
|
||||
# For standard LoraLoader nodes, strength is in next widget_value
|
||||
if (
|
||||
node_type in ["LoraLoader", "LoraLoaderModelOnly"]
|
||||
and isinstance(widget_index, int)
|
||||
ref.get("strength") is None
|
||||
and category in {"lora", "loras"}
|
||||
):
|
||||
# Find the node in workflow to get strength value
|
||||
for node in nodes:
|
||||
if node_matches_ref(node, ref):
|
||||
widgets_values = node.get("widgets_values", [])
|
||||
if (
|
||||
isinstance(widgets_values, list)
|
||||
and len(widgets_values) > widget_index + 1
|
||||
):
|
||||
try:
|
||||
strength = float(
|
||||
widgets_values[widget_index + 1]
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
strength = 1.0
|
||||
strength = get_lora_model_strength(
|
||||
node,
|
||||
widget_index,
|
||||
)
|
||||
break
|
||||
|
||||
if ref.get("strength") is not None:
|
||||
|
||||
@@ -960,6 +960,111 @@ def get_dynamic_serialized_widget_name(
|
||||
return names_by_index.get(widget_index, "")
|
||||
|
||||
|
||||
LORA_MODEL_STRENGTH_WIDGET_NAMES = (
|
||||
"strength_model",
|
||||
"lora_model_strength",
|
||||
"model_strength",
|
||||
"lora_strength",
|
||||
"strength",
|
||||
)
|
||||
|
||||
LEGACY_LORA_STRENGTH_NEXT_WIDGET_TYPES = {
|
||||
"LoraLoader",
|
||||
"LoraLoaderModelOnly",
|
||||
"LoraLoaderBypass",
|
||||
"LoraLoaderBypassModelOnly",
|
||||
"CreateHookLora",
|
||||
"CreateHookLoraModelOnly",
|
||||
}
|
||||
|
||||
|
||||
def _coerce_lora_strength(value: Any) -> Optional[float]:
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def get_lora_model_strength(
|
||||
node: Dict[str, Any], lora_widget_index: Optional[int] = None
|
||||
) -> Optional[float]:
|
||||
"""Return the model strength associated with a LoRA widget in a workflow node."""
|
||||
widgets_values = node.get("widgets_values", [])
|
||||
if not isinstance(widgets_values, list):
|
||||
return None
|
||||
|
||||
hints = get_dynamic_node_widget_category_hints(str(node.get("type", "") or ""))
|
||||
schema_names = _normalized_index_mapping(
|
||||
hints.get("serialized_name_by_index", {})
|
||||
)
|
||||
effective_names, _non_model_indices = _dynamic_serialized_widget_layout(
|
||||
node, hints
|
||||
)
|
||||
if effective_names:
|
||||
schema_names = effective_names
|
||||
|
||||
values_by_name: Dict[str, Any] = {}
|
||||
names_by_index: Dict[int, List[str]] = {}
|
||||
for widget_index, value in enumerate(widgets_values):
|
||||
names = []
|
||||
schema_name = schema_names.get(widget_index)
|
||||
if schema_name:
|
||||
names.append(schema_name)
|
||||
names.extend(get_widget_name_candidates(node, widget_index))
|
||||
normalized_names = unique_ordered_strings(
|
||||
[normalize_widget_name(name) for name in names]
|
||||
)
|
||||
names_by_index[widget_index] = normalized_names
|
||||
for normalized_name in normalized_names:
|
||||
if normalized_name and normalized_name not in values_by_name:
|
||||
values_by_name[normalized_name] = value
|
||||
|
||||
if isinstance(lora_widget_index, int):
|
||||
for widget_name in names_by_index.get(lora_widget_index, []):
|
||||
indexed_match = re.fullmatch(r"lora_(\d+)_name", widget_name)
|
||||
if not indexed_match:
|
||||
continue
|
||||
|
||||
lora_number = indexed_match.group(1)
|
||||
mode = str(values_by_name.get("mode") or "").strip().lower()
|
||||
if mode == "simple":
|
||||
indexed_strength_names = [
|
||||
f"lora_{lora_number}_strength",
|
||||
f"lora_{lora_number}_model_strength",
|
||||
]
|
||||
else:
|
||||
indexed_strength_names = [
|
||||
f"lora_{lora_number}_model_strength",
|
||||
f"lora_{lora_number}_strength",
|
||||
]
|
||||
|
||||
for strength_name in indexed_strength_names:
|
||||
strength = _coerce_lora_strength(
|
||||
values_by_name.get(strength_name)
|
||||
)
|
||||
if strength is not None:
|
||||
return strength
|
||||
|
||||
for strength_name in LORA_MODEL_STRENGTH_WIDGET_NAMES:
|
||||
if strength_name not in values_by_name:
|
||||
continue
|
||||
strength = _coerce_lora_strength(values_by_name[strength_name])
|
||||
if strength is not None:
|
||||
return strength
|
||||
|
||||
node_type = str(node.get("type", "") or "")
|
||||
if (
|
||||
node_type in LEGACY_LORA_STRENGTH_NEXT_WIDGET_TYPES
|
||||
and isinstance(lora_widget_index, int)
|
||||
and 0 <= lora_widget_index + 1 < len(widgets_values)
|
||||
):
|
||||
return _coerce_lora_strength(widgets_values[lora_widget_index + 1])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
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 ""))
|
||||
_names_by_index, non_model_indices = _dynamic_serialized_widget_layout(
|
||||
|
||||
@@ -54,6 +54,10 @@ const resolverDialogSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/resolver_dialog.js'),
|
||||
'utf8'
|
||||
);
|
||||
const workflowStateMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/shell/workflow_state_methods.js'),
|
||||
'utf8'
|
||||
);
|
||||
const renderFormatMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/utils/render_format_methods.js'),
|
||||
'utf8'
|
||||
@@ -535,6 +539,94 @@ function extractMethod(source, methodName, paramsPattern = '[^)]*') {
|
||||
throw new Error(`Could not parse ${methodName}`);
|
||||
}
|
||||
|
||||
test('workflow signature tracks model strength for bypass and custom LoRA loaders', () => {
|
||||
const getWorkflowSignatureData = eval(`(${extractMethod(workflowStateMethodsSource, 'getWorkflowSignatureData')})`);
|
||||
const dialog = { capabilities: { node_rules: {} } };
|
||||
const makeWorkflow = (type, strengthName, strength) => ({
|
||||
nodes: [{
|
||||
id: 34,
|
||||
type,
|
||||
inputs: [
|
||||
{ name: 'model', type: 'MODEL', link: 1 },
|
||||
{ name: 'clip', type: 'CLIP', link: 2 },
|
||||
{ name: 'lora_name', widget: { name: 'lora_name' } },
|
||||
{ name: strengthName, widget: { name: strengthName } },
|
||||
],
|
||||
outputs: [],
|
||||
widgets_values: ['AnimeEditV2.safetensors', strength],
|
||||
}],
|
||||
links: [],
|
||||
});
|
||||
|
||||
const bypass = getWorkflowSignatureData.call(
|
||||
dialog,
|
||||
makeWorkflow('LoraLoaderBypass', 'strength_model', 0.65)
|
||||
);
|
||||
const custom = getWorkflowSignatureData.call(
|
||||
dialog,
|
||||
makeWorkflow('easy fullLoader', 'lora_model_strength', 0.8)
|
||||
);
|
||||
|
||||
assert.deepEqual(bypass.nodes[0].widgets_values, [
|
||||
{ index: 0, value: 'AnimeEditV2.safetensors' },
|
||||
{ index: 1, value: 0.65 },
|
||||
]);
|
||||
assert.deepEqual(custom.nodes[0].widgets_values, [
|
||||
{ index: 0, value: 'AnimeEditV2.safetensors' },
|
||||
{ index: 1, value: 0.8 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('workflow signature tracks EasyLoraStack mode and indexed strengths', () => {
|
||||
const getWorkflowSignatureData = eval(`(${extractMethod(workflowStateMethodsSource, 'getWorkflowSignatureData')})`);
|
||||
const dialog = {
|
||||
capabilities: {
|
||||
node_rules: {
|
||||
'easy loraStack': { 3: 'loras', 7: 'loras' },
|
||||
},
|
||||
},
|
||||
};
|
||||
const widgetNames = [
|
||||
'toggle',
|
||||
'mode',
|
||||
'num_loras',
|
||||
'lora_1_name',
|
||||
'lora_1_strength',
|
||||
'lora_1_model_strength',
|
||||
'lora_1_clip_strength',
|
||||
];
|
||||
const workflow = {
|
||||
nodes: [{
|
||||
id: 160,
|
||||
type: 'easy loraStack',
|
||||
inputs: widgetNames.map(name => ({ name, widget: { name } })),
|
||||
outputs: [],
|
||||
widgets_values: [
|
||||
true,
|
||||
'advanced',
|
||||
1,
|
||||
'nsfw_flux_lora_v1.safetensors',
|
||||
1,
|
||||
0.45,
|
||||
0.25,
|
||||
],
|
||||
}],
|
||||
links: [],
|
||||
};
|
||||
|
||||
const signature = getWorkflowSignatureData.call(dialog, workflow);
|
||||
|
||||
assert.deepEqual(signature.nodes[0].widgets_values, [
|
||||
{ index: 0, value: true },
|
||||
{ index: 1, value: 'advanced' },
|
||||
{ index: 2, value: 1 },
|
||||
{ index: 3, value: 'nsfw_flux_lora_v1.safetensors' },
|
||||
{ index: 4, value: 1 },
|
||||
{ index: 5, value: 0.45 },
|
||||
{ index: 6, value: 0.25 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('download percent keeps native Xet progress below one percent visible', () => {
|
||||
const formatDownloadPercent = eval(`(${extractMethod(renderFormatMethodsSource, 'formatDownloadPercent')})`);
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ from unittest.mock import patch
|
||||
|
||||
from core import workflow_analyzer
|
||||
from core.scanner import scan_directory
|
||||
from core.workflow_analyzer import analyze_workflow_models, identify_missing_models
|
||||
from core.workflow_analyzer import (
|
||||
analyze_workflow_models,
|
||||
get_lora_model_strength,
|
||||
identify_missing_models,
|
||||
)
|
||||
|
||||
|
||||
def _workflow_with_model(model_path):
|
||||
@@ -995,6 +999,145 @@ class WorkflowCategoryHintsTests(unittest.TestCase):
|
||||
self.assertEqual("loras", NODE_TYPE_TO_CATEGORY_HINTS.get("LoraLoaderV2"))
|
||||
|
||||
|
||||
class WorkflowLoraStrengthTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def _easy_lora_stack_hints():
|
||||
return {
|
||||
"serialized_name_by_index": {
|
||||
0: "toggle",
|
||||
1: "mode",
|
||||
2: "num_loras",
|
||||
3: "lora_1_name",
|
||||
4: "lora_1_strength",
|
||||
5: "lora_1_model_strength",
|
||||
6: "lora_1_clip_strength",
|
||||
7: "lora_2_name",
|
||||
8: "lora_2_strength",
|
||||
9: "lora_2_model_strength",
|
||||
10: "lora_2_clip_strength",
|
||||
},
|
||||
"non_model_by_index": [],
|
||||
"has_generated_widgets": False,
|
||||
}
|
||||
|
||||
def test_bypass_loader_uses_strength_model_from_dynamic_schema(self):
|
||||
node = {
|
||||
"type": "LoraLoaderBypass",
|
||||
"widgets_values": [
|
||||
r"Anima\anime\AnimeEditV2.safetensors",
|
||||
0.65,
|
||||
0.4,
|
||||
],
|
||||
}
|
||||
hints = {
|
||||
"serialized_name_by_index": {
|
||||
0: "lora_name",
|
||||
1: "strength_model",
|
||||
2: "strength_clip",
|
||||
},
|
||||
"non_model_by_index": [],
|
||||
"has_generated_widgets": False,
|
||||
}
|
||||
|
||||
with patch(
|
||||
"core.workflow_analyzer.get_dynamic_node_widget_category_hints",
|
||||
return_value=hints,
|
||||
):
|
||||
strength = get_lora_model_strength(node, 0)
|
||||
|
||||
self.assertEqual(0.65, strength)
|
||||
|
||||
def test_custom_loader_supports_lora_model_strength_widget(self):
|
||||
node = {
|
||||
"type": "easy fullLoader",
|
||||
"inputs": [
|
||||
{"name": "model", "type": "MODEL"},
|
||||
{"name": "lora_name", "widget": {"name": "lora_name"}},
|
||||
{
|
||||
"name": "lora_model_strength",
|
||||
"widget": {"name": "lora_model_strength"},
|
||||
},
|
||||
{
|
||||
"name": "lora_clip_strength",
|
||||
"widget": {"name": "lora_clip_strength"},
|
||||
},
|
||||
],
|
||||
"widgets_values": ["style.safetensors", 0.8, 0.3],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"core.workflow_analyzer.get_dynamic_node_widget_category_hints",
|
||||
return_value={},
|
||||
):
|
||||
strength = get_lora_model_strength(node, 0)
|
||||
|
||||
self.assertEqual(0.8, strength)
|
||||
|
||||
def test_bypass_loader_keeps_legacy_next_widget_fallback(self):
|
||||
node = {
|
||||
"type": "LoraLoaderBypass",
|
||||
"widgets_values": ["style.safetensors", "1.25", 0.5],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"core.workflow_analyzer.get_dynamic_node_widget_category_hints",
|
||||
return_value={},
|
||||
):
|
||||
strength = get_lora_model_strength(node, 0)
|
||||
|
||||
self.assertEqual(1.25, strength)
|
||||
|
||||
def test_easy_lora_stack_simple_mode_uses_matching_indexed_strength(self):
|
||||
node = {
|
||||
"type": "easy loraStack",
|
||||
"widgets_values": [
|
||||
True,
|
||||
"simple",
|
||||
2,
|
||||
"first.safetensors",
|
||||
0.7,
|
||||
0.2,
|
||||
0.3,
|
||||
"second.safetensors",
|
||||
1.1,
|
||||
1.2,
|
||||
1.3,
|
||||
],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"core.workflow_analyzer.get_dynamic_node_widget_category_hints",
|
||||
return_value=self._easy_lora_stack_hints(),
|
||||
):
|
||||
first_strength = get_lora_model_strength(node, 3)
|
||||
second_strength = get_lora_model_strength(node, 7)
|
||||
|
||||
self.assertEqual(0.7, first_strength)
|
||||
self.assertEqual(1.1, second_strength)
|
||||
|
||||
def test_easy_lora_stack_advanced_mode_uses_model_strength(self):
|
||||
node = {
|
||||
"type": "easy loraStack",
|
||||
"widgets_values": [
|
||||
True,
|
||||
"advanced",
|
||||
1,
|
||||
"style.safetensors",
|
||||
0.7,
|
||||
0.45,
|
||||
0.25,
|
||||
],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"core.workflow_analyzer.get_dynamic_node_widget_category_hints",
|
||||
return_value=self._easy_lora_stack_hints(),
|
||||
):
|
||||
strength = get_lora_model_strength(node, 3)
|
||||
|
||||
self.assertEqual(0.45, strength)
|
||||
|
||||
|
||||
class WorkflowMissingReferenceGroupingTests(unittest.TestCase):
|
||||
def test_power_lora_duplicate_missing_tracks_all_node_refs(self):
|
||||
workflow = {
|
||||
|
||||
@@ -66,7 +66,23 @@ export const workflowStateMethods = {
|
||||
'active',
|
||||
'on'
|
||||
]);
|
||||
const loraStrengthNodeTypes = new Set(['LoraLoader', 'LoraLoaderModelOnly']);
|
||||
const loraStrengthNodeTypes = new Set([
|
||||
'LoraLoader',
|
||||
'LoraLoaderModelOnly',
|
||||
'LoraLoaderBypass',
|
||||
'LoraLoaderBypassModelOnly',
|
||||
'CreateHookLora',
|
||||
'CreateHookLoraModelOnly'
|
||||
]);
|
||||
const loraStrengthKeys = new Set([
|
||||
'strength_model',
|
||||
'lora_model_strength',
|
||||
'model_strength',
|
||||
'lora_strength',
|
||||
'strength'
|
||||
]);
|
||||
const indexedLoraStrengthPattern = /^lora_\d+_(?:strength|model_strength|clip_strength)$/;
|
||||
const loraStackStateKeys = new Set(['toggle', 'mode', 'num_loras']);
|
||||
const staticFallback = {
|
||||
CheckpointLoaderSimple: [0],
|
||||
CheckpointLoader: [1],
|
||||
@@ -158,7 +174,13 @@ export const workflowStateMethods = {
|
||||
|| urlPattern.test(text)
|
||||
|| loraTokenPattern.test(text);
|
||||
};
|
||||
const isRelevantScalarKey = (key = '') => ['strength', 'active', 'on', 'model_id', 'modelid', 'version_id', 'versionid'].includes(normalizeKey(key));
|
||||
const isRelevantScalarKey = (key = '') => {
|
||||
const normalizedKey = normalizeKey(key);
|
||||
return loraStrengthKeys.has(normalizedKey)
|
||||
|| indexedLoraStrengthPattern.test(normalizedKey)
|
||||
|| loraStackStateKeys.has(normalizedKey)
|
||||
|| ['active', 'on', 'model_id', 'modelid', 'version_id', 'versionid'].includes(normalizedKey);
|
||||
};
|
||||
const normalizeRelevantValue = (value, key = '', isModelWidget = false) => {
|
||||
if (value == null) return null;
|
||||
|
||||
@@ -192,8 +214,14 @@ export const workflowStateMethods = {
|
||||
const widgetsValues = Array.isArray(node.widgets_values) ? node.widgets_values : [];
|
||||
const nodeType = node.type || '';
|
||||
const widgetValueKey = (index) => {
|
||||
for (const key of ['widgets', 'inputs']) {
|
||||
const items = Array.isArray(node[key]) ? node[key] : [];
|
||||
const widgetInputs = Array.isArray(node.inputs)
|
||||
? node.inputs.filter(item => item?.widget != null)
|
||||
: [];
|
||||
const collections = [
|
||||
Array.isArray(node.widgets) ? node.widgets : [],
|
||||
widgetInputs
|
||||
];
|
||||
for (const items of collections) {
|
||||
const item = items[index];
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const widgetName = item.widget && typeof item.widget === 'object'
|
||||
@@ -208,7 +236,13 @@ export const workflowStateMethods = {
|
||||
.map((value, index) => {
|
||||
const key = widgetValueKey(index);
|
||||
const isModelWidget = isModelWidgetIndex(nodeType, index);
|
||||
let normalized = normalizeRelevantValue(value, key, isModelWidget);
|
||||
const isLoraStackState = nodeType === 'easy loraStack'
|
||||
&& loraStackStateKeys.has(normalizeKey(key));
|
||||
let normalized = normalizeRelevantValue(
|
||||
value,
|
||||
key,
|
||||
isModelWidget || isLoraStackState
|
||||
);
|
||||
|
||||
if (
|
||||
normalized === null
|
||||
|
||||
Reference in New Issue
Block a user