Files
DemonGatanjieu-Anomalous_Mo…/web/modules/recipe_identity.js
T
DemonGatanjieuandClaude Opus 5.5 c59927e701 Read the LoRAs of popular third-party nodes wherever AMB reads a workflow's models
AMB read models only from ordinary drop-downs, so rgthree's Power Lora Loader (rows are
{on, lora, strength} objects) was invisible: Model Check never reported its missing LoRAs.
Now its switched-on rows are Model Check boxes (lora_nodes.js powerLoraRowBoxes: ComfyUI's LoRA
list as choices, a fix changes only the row's LoRA; downloads put files in the same way), saved
fingerprints and a picture's model search read them, the workflow summary lists them, and saved
model references include them and Easy-Use LoRA stack rows (nodeModelValues /
_model_reference_values, mirrored). Only these widely used nodes; ComfyUI's loaders stay the
reference.

recipe_parser.js was over 600 lines: which LoRAs a node loads moves to recipe_loras.js and the
box readers to recipe_widgets.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:57:36 +08:00

192 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Pure helpers for recipe model references and provenance labels. */
const MODEL_FILE_PATTERN = /\.(?:safetensors|ckpt|pt|bin|sft)$/i;
const VERIFIABLE_RECIPE_MODEL_CATEGORIES = new Set([
'checkpoint',
'lora',
'unet',
'controlnet',
'vae',
'text_encoder',
'clip_vision',
]);
function nodeType(node) {
return String(node?.type || node?.class_type || '').trim();
}
function nodeTitle(node) {
return String(node?._meta?.title || node?.title || nodeType(node) || 'Unknown node').trim();
}
/**
* All-in-one loaders with a verified serialized widget layout. Must stay in sync
* with ALL_IN_ONE_LOADER_SPECS in api/recipe_schema.py. Arbitrary third-party
* widgets stay parameters: guessing categories from file names mislabels models.
*/
export const ALL_IN_ONE_LOADER_SPECS = Object.freeze({
// ComfyUI-Easy-Use: ckpt_name, vae_name, clip_skip, lora_name, ...
'easy a1111loader': [[0, 'checkpoint', 'ckpt_name'], [1, 'vae', 'vae_name'], [3, 'lora', 'lora_name']],
// ComfyUI-Easy-Use: ckpt_name, config_name, vae_name, clip_skip, lora_name, ...
'easy fullloader': [[0, 'checkpoint', 'ckpt_name'], [2, 'vae', 'vae_name'], [4, 'lora', 'lora_name']],
// efficiency-nodes-comfyui: ckpt_name, vae_name, clip_skip, lora_name, ...
'efficient loader': [[0, 'checkpoint', 'ckpt_name'], [1, 'vae', 'vae_name'], [3, 'lora', 'lora_name']],
});
/** Loader values that mean "no file selected". */
const PLACEHOLDER_MODEL_VALUES = new Set(['none', 'baked vae']);
export function isAllInOneLoaderType(type) {
return Object.prototype.hasOwnProperty.call(ALL_IN_ONE_LOADER_SPECS, String(type || '').trim().toLowerCase());
}
export function isPlaceholderModelValue(value) {
return PLACEHOLDER_MODEL_VALUES.has(String(value ?? '').trim().toLowerCase());
}
/** [widgetIndex, category, widgetName] triples for known loader nodes (by type). */
export function deriveNodeModelSpecs(nodeOrType) {
const type = typeof nodeOrType === 'string' ? nodeOrType : nodeType(nodeOrType);
const lowered = type.trim().toLowerCase();
if (isAllInOneLoaderType(lowered)) return ALL_IN_ONE_LOADER_SPECS[lowered].map((spec) => [...spec]);
if (/checkpointloader(simple)?$/.test(lowered)) return [[0, 'checkpoint', 'checkpoint']];
if (lowered.endsWith('unetloader')) return [[0, 'unet', 'unet']];
if (/loraloader/.test(lowered)) return [[0, 'lora', 'lora']];
if (lowered.endsWith('vaeloader')) return [[0, 'vae', 'vae']];
if (lowered.endsWith('clipvisionloader')) return [[0, 'clip_vision', 'clip_vision']];
if (lowered.endsWith('controlnetloader')) return [[0, 'controlnet', 'controlnet']];
if (/(^|[^a-z])(?:dual|triple)?cliploader$/.test(lowered)) {
const specs = [[0, 'text_encoder', 'clip']];
if (lowered.includes('dualclip') || lowered.includes('tripleclip')) specs.push([1, 'text_encoder', 'clip']);
if (lowered.includes('tripleclip')) specs.push([2, 'text_encoder', 'clip']);
return specs;
}
return [];
}
const POWER_LORA_TYPE = 'power lora loader (rgthree)';
const EASY_LORA_STACK_TYPE = 'easy lorastack';
const EASY_STACK_ROWS = 10;
/**
* Every model a saved node names: [widgetIndex, category, widgetName, value]. Known loaders by
* position (deriveNodeModelSpecs); two stack nodes by their rows: rgthree's Power Lora Loader (a
* row is an object { on, lora, strength }; switched-off rows load nothing) and Easy-Use's LoRA
* stack (lora_N_name at 3 + 4·(N−1), up to num_loras). Must stay in sync with
* _model_reference_values in api/recipe_schema.py.
*/
export function nodeModelValues(node) {
const values = Array.isArray(node?.widgets_values) ? node.widgets_values : [];
const lowered = nodeType(node).toLowerCase();
if (lowered === POWER_LORA_TYPE) {
const rows = values.flatMap((value, index) => (value && typeof value === 'object' && 'lora' in value ? [[index, value]] : []));
return rows.flatMap(([index, value], row) => (value.on === false ? [] : [[index, 'lora', `lora_${row + 1}`, value.lora]]));
}
if (lowered === EASY_LORA_STACK_TYPE) {
const count = Math.min(Math.max(Math.trunc(Number(values[2])) || 0, 0), EASY_STACK_ROWS);
return Array.from({ length: count }, (_, row) => [3 + 4 * row, 'lora', `lora_${row + 1}_name`, values[3 + 4 * row]]);
}
return deriveNodeModelSpecs(node).map(([index, category, name]) => [index, category, name, values[index]]);
}
function statusFor(identity) {
const status = identity?.status;
return ['verified', 'unverified', 'unavailable'].includes(status) ? status : 'unavailable';
}
export function normaliseIdentity(identity) {
const result = { ...(identity || {}) };
result.status = statusFor(identity);
if (typeof result.sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(result.sha256)) delete result.sha256;
if (!Number.isFinite(Number(result.size))) delete result.size;
return result;
}
function workflowIdentity(workflow, nodeId, savedValue) {
const hashes = workflow?.extra?.anomalous_hashes;
if (!hashes || typeof savedValue !== 'string') return null;
const normalized = savedValue.replace(/\\/g, '/');
const windowsPath = savedValue.replace(/\//g, '\\');
const record = hashes[`${nodeId}_${savedValue}`]
|| hashes[`${nodeId}_${normalized}`]
|| hashes[`${nodeId}_${windowsPath}`]
|| hashes[savedValue]
|| hashes[normalized]
|| hashes[windowsPath];
const sha256 = typeof record === 'string' ? record : record?.hash;
if (typeof sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(sha256)) return null;
const identity = {
status: 'verified',
sha256: sha256.toLowerCase(),
provenance: 'workflow snapshot',
};
if (record?.size !== null && record?.size !== '' && Number.isFinite(Number(record?.size))) {
identity.size = Number(record.size);
}
return identity;
}
export function canVerifyRecipeModelReference(reference) {
return VERIFIABLE_RECIPE_MODEL_CATEGORIES.has(String(reference?.category || '').toLowerCase());
}
export function deriveRecipeModelReferences(recipe) {
const stored = recipe?.params?.model_references;
if (Array.isArray(stored) && stored.length) {
return stored.map((reference) => ({
...reference,
identity: normaliseIdentity(reference.identity),
}));
}
const references = [];
for (const node of recipe?.workflow?.nodes || []) {
const type = nodeType(node);
for (const [widgetIndex, category, widgetName, savedValue] of nodeModelValues(node)) {
if (typeof savedValue !== 'string' || !savedValue.trim() || isPlaceholderModelValue(savedValue)) continue;
const identity = workflowIdentity(recipe?.workflow, node?.id, savedValue)
|| { status: 'unverified' };
references.push({
node_id: node?.id ?? null,
node_type: type || 'Unknown',
node_title: nodeTitle(node),
widget_index: widgetIndex,
widget_name: widgetName,
saved_value: savedValue,
category,
base_model: recipe?.params?.baseModel || null,
identity,
});
}
}
return references;
}
export function shortHash(value) {
if (typeof value !== 'string' || !value) return '';
return value.length > 16 ? `${value.slice(0, 12)}...${value.slice(-4)}` : value;
}
export function recipeReferenceKey(reference) {
return [
reference?.node_id ?? '',
reference?.widget_index ?? '',
reference?.category ?? '',
reference?.saved_value ?? '',
].join('\u001f');
}
export function formatIdentitySize(value) {
const size = Number(value);
if (!Number.isFinite(size) || size < 0) return '';
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
export function isModelReference(reference) {
const val = String(reference?.saved_value || '').trim();
return Boolean(val && !isPlaceholderModelValue(val) && MODEL_FILE_PATTERN.test(val));
}