From 552ef683da042091968ddd9f25d9c8322c4e2cf1 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 13 Sep 2026 23:09:22 +0200 Subject: [PATCH] Unify metadata workflow cache and category handling --- core/services/image_metadata_service.py | 21 +-- core/type_utils.py | 3 + core/workflow_transfer.py | 39 +---- tests/test_downloads_tab_workflow_route.mjs | 59 ++++++++ tests/test_image_metadata_transfer.mjs | 117 +++++++++++++++ tests/test_workflow_transfer.py | 7 + web/resolver/shell/workflow_state_methods.js | 12 ++ .../utils/category_aliases.generated.js | 3 + web/resolver/utils/category_utils.js | 17 +++ .../views/tabs_image_metadata_methods.js | 138 +++++++++++------- web/resolver/views/tabs_loaded_methods.js | 13 +- 11 files changed, 318 insertions(+), 111 deletions(-) diff --git a/core/services/image_metadata_service.py b/core/services/image_metadata_service.py index 0e57a99..578ba8d 100644 --- a/core/services/image_metadata_service.py +++ b/core/services/image_metadata_service.py @@ -14,6 +14,7 @@ from ..request_utils import ( read_text_field, ) from ..routes.context import RouteContext +from ..type_utils import CATEGORY_MAP, normalize_category_token from ..workflow.formats import normalize_workflow_payload MAX_IMAGE_BYTES = 64 * 1024 * 1024 @@ -25,22 +26,6 @@ _CIVITAI_JSON_LABELS = ( "Civitai metadata", "Civitai resources", ) -_CIVITAI_CATEGORY_ALIASES = { - "checkpoint": "checkpoints", - "checkpoints": "checkpoints", - "lora": "loras", - "loras": "loras", - "vae": "vae", - "controlnet": "controlnet", - "embedding": "embeddings", - "embeddings": "embeddings", - "textualinversion": "embeddings", - "upscale": "upscale_models", - "upscaler": "upscale_models", - "upscalemodel": "upscale_models", -} - - def _decode_exif_user_comment(value: bytes) -> str: """Decode the standard EXIF UserComment prefix and payload.""" if value.startswith(b"UNICODE"): @@ -268,8 +253,8 @@ def _normalize_civitai_resource(value: Any, index: int) -> Optional[dict[str, An resource_type = str( _first_mapping_value(value, ("type", "model_type", "modelType")) or "" ).strip() - category_key = re.sub(r"[^a-z0-9]+", "", resource_type.lower()) - category = _CIVITAI_CATEGORY_ALIASES.get(category_key, category_key or "unknown") + category_key = normalize_category_token(resource_type) + category = CATEGORY_MAP.get(category_key, category_key or "unknown") model_version_id = _coerce_identifier( _first_mapping_value( value, diff --git a/core/type_utils.py b/core/type_utils.py index e8b43b5..6714297 100644 --- a/core/type_utils.py +++ b/core/type_utils.py @@ -242,6 +242,7 @@ URN_TYPE_MAP = { CATEGORY_MAP = { "checkpoints": "checkpoints", "checkpoint": "checkpoints", + "ckpt": "checkpoints", "loras": "loras", "lora": "loras", "embeddings": "embeddings", @@ -254,7 +255,9 @@ CATEGORY_MAP = { "control_net": "controlnet", "vae": "vae", "upscaler": "upscale_models", + "upscale": "upscale_models", "upscale_model": "upscale_models", + "upscalemodel": "upscale_models", "upscale_models": "upscale_models", "latent_upscale_model": "latent_upscale_models", "latent_upscale_models": "latent_upscale_models", diff --git a/core/workflow_transfer.py b/core/workflow_transfer.py index 261b105..c55a5fc 100644 --- a/core/workflow_transfer.py +++ b/core/workflow_transfer.py @@ -11,33 +11,11 @@ from typing import Any, Dict, List, Optional from .contracts import ModelReference, Resolution, ResolvedModel from .custom_nodes import get_custom_node_resolution_metadata from .path_utils import get_filename_from_path +from .type_utils import CATEGORY_MAP, normalize_category_token from .workflow import dynamic_widgets from .workflow.widgets import get_widget_name_candidates, normalize_widget_name from .workflow_updater import update_model_path -_CATEGORY_ALIASES = { - "checkpoint": "checkpoints", - "checkpoints": "checkpoints", - "ckpt": "checkpoints", - "lora": "loras", - "loras": "loras", - "text_encoder": "text_encoders", - "text_encoders": "text_encoders", - "clip": "text_encoders", - "vae": "vae", - "controlnet": "controlnet", - "control_net": "controlnet", - "upscale": "upscale_models", - "upscaler": "upscale_models", - "upscale_model": "upscale_models", - "upscale_models": "upscale_models", - "diffusion_model": "diffusion_models", - "diffusion_models": "diffusion_models", - "unet": "diffusion_models", - "embedding": "embeddings", - "embeddings": "embeddings", -} - _LORA_MANAGER_NODE_TYPES = { "LoraLoaderV2", "Lora Loader (LoraManager)", @@ -64,10 +42,10 @@ _TRANSFER_ACTIVITY_SCOPES = {"all", "active", "inactive"} def normalize_transfer_category(value: Any) -> str: """Normalize the category names used by workflow and metadata payloads.""" - token = str(value or "").strip().lower().replace("-", "_") + token = normalize_category_token(value) if not token or token in {"unknown", "none", "null"}: return "" - return _CATEGORY_ALIASES.get(token, token) + return CATEGORY_MAP.get(token, token) def _normalize_identity(value: Any) -> str: @@ -289,17 +267,6 @@ def _resolution_payload(resolution: Resolution) -> Dict[str, Any]: return payload -def _assign_sources( - sources: Sequence[Mapping[str, Any]], - references: Sequence[ModelReference], -) -> List[tuple[ModelReference, Mapping[str, Any]]]: - if not sources or not references: - return [] - if len(sources) == 1: - return [(reference, sources[0]) for reference in references] - return list(zip(references, sources, strict=False)) - - def _select_source_for_reference( category_sources: Sequence[Mapping[str, Any]], selector: Mapping[str, Any], diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 608e62c..48fe062 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -2096,6 +2096,40 @@ test('background Loaded Models refresh keeps the current view until new data is assert.equal(contentElement.scrollTop, 48); }); +test('Loaded Models renders the shared cache without starting another scan', async () => { + const loadLoadedModels = eval(`(${extractMethod(tabsLoadedMethodsSource, 'loadLoadedModels')})`); + const getCachedLoadedModelsForSignature = eval(`(${extractMethod( + workflowStateMethodsSource, + 'getCachedLoadedModelsForSignature' + )})`); + const cachedData = { loaded_models: [{ strength: 0.5 }], total: 1 }; + const contentElement = { innerHTML: '' }; + let fetchCount = 0; + let renderCount = 0; + const dialog = { + activeTab: 'loaded', + contentElement, + cachedLoadedModelsSignature: 'workflow-a', + cachedLoadedModelsData: cachedData, + syncWorkflowScopedQueue() {}, + getWorkflowSignature: () => 'workflow-a', + getCachedLoadedModelsForSignature, + fetchJson: async () => { + fetchCount += 1; + throw new Error('The shared cache was not used'); + }, + displayLoadedModels: (_container, data) => { + renderCount += 1; + assert.equal(data, cachedData); + }, + }; + + await loadLoadedModels.call(dialog, { nodes: [] }); + + assert.equal(fetchCount, 0); + assert.equal(renderCount, 1); +}); + test('Loaded Models progress patches the stable progress container', () => { const pollWorkflowProgress = extractMethod(renderFormatMethodsSource, 'pollWorkflowProgress'); const patchLoadedModelsProgress = extractMethod( @@ -2730,6 +2764,31 @@ test('workflow analysis and loaded model caches stay independent and cloned', () assert.equal(dialog.workflowAnalysisCaches.get(dialog.workflowKey).data.missing_models.length, 1); }); +test('shared loaded model cache requires a matching workflow signature unless forced', () => { + const getCachedLoadedModelsForSignature = eval(`(${extractMethod( + workflowStateMethodsSource, + 'getCachedLoadedModelsForSignature' + )})`); + const cachedData = { loaded_models: [{ filename: 'loaded.safetensors' }] }; + const dialog = { + cachedLoadedModelsSignature: 'workflow-a', + cachedLoadedModelsData: cachedData, + }; + + assert.equal( + getCachedLoadedModelsForSignature.call(dialog, 'workflow-a'), + cachedData + ); + assert.equal( + getCachedLoadedModelsForSignature.call(dialog, 'workflow-b'), + null + ); + assert.equal( + getCachedLoadedModelsForSignature.call(dialog, 'workflow-a', { force: true }), + null + ); +}); + test('Missing Models filter toggles persist state and rerender once per change', () => { const bindMissingFilterToggle = eval( `(${extractMethod(missingBrowserMethodsSource, 'bindMissingFilterToggle')})` diff --git a/tests/test_image_metadata_transfer.mjs b/tests/test_image_metadata_transfer.mjs index 81659cc..be43916 100644 --- a/tests/test_image_metadata_transfer.mjs +++ b/tests/test_image_metadata_transfer.mjs @@ -147,6 +147,123 @@ function createLoraTransferState(sourceModels, activityScope = 'all') { }; } +test('metadata transfer persists only mode and activity scope preferences', () => { + const context = createTransferContext(); + + assert.deepEqual(context.getMetadataTransferPreferences(), { + mode: 'replace', + activityScope: 'all', + }); + + assert.equal(context.setMetadataTransferMode('merge'), 'merge'); + assert.equal(context.setMetadataTransferActivityScope('inactive'), 'inactive'); + assert.deepEqual(context.getMetadataTransferPreferences(), { + mode: 'merge', + activityScope: 'inactive', + }); +}); + +test('metadata tab reuses transfer state for the unchanged workflow', () => { + const context = createTransferContext(); + context.getWorkflowSignature = workflow => workflow.signature; + const state = { transfer: { workflowSignature: 'workflow-a' } }; + + assert.equal( + context.isMetadataTransferCurrentWorkflow(state, { signature: 'workflow-a' }), + true + ); + assert.equal( + context.isMetadataTransferCurrentWorkflow(state, { signature: 'workflow-b' }), + false + ); + assert.equal( + context.isMetadataTransferCurrentWorkflow({ transfer: { workflowSignature: '' } }, null), + true + ); + assert.equal(context.isMetadataTransferCurrentWorkflow(null, null), false); +}); + +test('metadata transfer reuses the shared Loaded Models cache for the same workflow', async () => { + const cachedData = { + loaded_models: [{ + category: 'text_encoders', + node_id: 1, + node_type: 'CLIPLoader', + widget_index: 0, + original_path: 'current.safetensors', + active: true, + connected: true, + is_top_level: true, + }], + total: 1, + }; + let fetchCount = 0; + const context = { + ...imageMetadataMethods, + getCurrentWorkflow: () => ({ signature: 'workflow-a' }), + getWorkflowSignature: workflow => workflow.signature, + cachedLoadedModelsSignature: 'workflow-a', + cachedLoadedModelsData: cachedData, + getCachedLoadedModelsForSignature(signature, { force = false } = {}) { + return !force && signature === this.cachedLoadedModelsSignature + ? this.cachedLoadedModelsData + : null; + }, + fetchJson: async () => { + fetchCount += 1; + throw new Error('The shared cache was not used'); + }, + renderImageMetadataResult() {}, + }; + + const result = await context.openMetadataTransfer(); + + assert.equal(fetchCount, 0); + assert.equal(result, cachedData); + assert.equal(context.imageInspectorState.transfer.targetModels, cachedData); +}); + +test('metadata transfer stores a fresh target scan in the shared Loaded Models cache', async () => { + const freshData = { loaded_models: [], total: 0 }; + let saveCount = 0; + const context = { + ...imageMetadataMethods, + getCurrentWorkflow: () => ({ signature: 'workflow-b' }), + getWorkflowSignature: workflow => workflow.signature, + cachedLoadedModelsSignature: null, + cachedLoadedModelsData: null, + getCachedLoadedModelsForSignature() { + return null; + }, + fetchJson: async () => freshData, + saveLoadedModelsCacheForActiveWorkflow() { + saveCount += 1; + }, + renderImageMetadataResult() {}, + }; + + const result = await context.openMetadataTransfer(); + + assert.equal(result, freshData); + assert.equal(context.cachedLoadedModelsSignature, 'workflow-b'); + assert.equal(context.cachedLoadedModelsData, freshData); + assert.equal(saveCount, 1); +}); + +test('metadata transfer uses the shared canonical category aliases', () => { + const context = createTransferContext(); + const state = createTransferState([ + { category: 'clip_gguf', original_path: 'clip.safetensors' }, + ]); + state.transfer.targetGroups[0].category = 'clips'; + state.transfer.targetGroups[0].nodes[0].refs[0].category = 'clips'; + + const [row] = context.getMetadataTransferPreviewRows(state); + + assert.equal(row.sourceUnavailable, false); + assert.equal(row.nextValue, 'clip.safetensors'); +}); + test('metadata transfer hides imported models with an unsupported file extension', () => { const context = createTransferContext(); const state = createTransferState([ diff --git a/tests/test_workflow_transfer.py b/tests/test_workflow_transfer.py index ecfcb3f..222fc7f 100644 --- a/tests/test_workflow_transfer.py +++ b/tests/test_workflow_transfer.py @@ -4,6 +4,7 @@ from types import SimpleNamespace import pytest from core.contracts import ModelReference +from core.type_utils import CATEGORY_MAP from core.workflow_transfer import ( normalize_transfer_category, transfer_models_to_workflow, @@ -48,9 +49,15 @@ def _selector(reference): def test_normalize_transfer_category_covers_workflow_aliases(): assert normalize_transfer_category("UPSCALE-MODEL") == "upscale_models" assert normalize_transfer_category("checkpoint") == "checkpoints" + assert normalize_transfer_category("CLIP / GGUF") == "text_encoders" assert normalize_transfer_category("unknown") == "" +def test_normalize_transfer_category_matches_the_canonical_category_map(): + for raw_category, canonical_category in CATEGORY_MAP.items(): + assert normalize_transfer_category(raw_category) == canonical_category + + def test_replace_fans_one_source_to_selected_slots_and_syncs_named_values(): workflow = { "nodes": [ diff --git a/web/resolver/shell/workflow_state_methods.js b/web/resolver/shell/workflow_state_methods.js index 3c8fecf..bda04a3 100644 --- a/web/resolver/shell/workflow_state_methods.js +++ b/web/resolver/shell/workflow_state_methods.js @@ -486,6 +486,18 @@ export const workflowStateMethods = { }; }, + getCachedLoadedModelsForSignature(workflowSignature, { force = false } = {}) { + if ( + force + || !workflowSignature + || this.cachedLoadedModelsSignature !== workflowSignature + || !this.cachedLoadedModelsData + ) { + return null; + } + return this.cachedLoadedModelsData; + }, + saveAnalysisCacheForActiveWorkflow() { this.saveWorkflowModelCache( this.workflowAnalysisCaches, diff --git a/web/resolver/utils/category_aliases.generated.js b/web/resolver/utils/category_aliases.generated.js index 4ad1591..4d28ce6 100644 --- a/web/resolver/utils/category_aliases.generated.js +++ b/web/resolver/utils/category_aliases.generated.js @@ -16,6 +16,7 @@ export const CATEGORY_ALIASES = Object.freeze( "background_removal_model": "background_removal", "checkpoint": "checkpoints", "checkpoints": "checkpoints", + "ckpt": "checkpoints", "clip": "text_encoders", "clip_gguf": "text_encoders", "clip_vision": "clip_vision", @@ -67,8 +68,10 @@ export const CATEGORY_ALIASES = Object.freeze( "ultralytics_segm": "ultralytics", "unet": "diffusion_models", "unet_gguf": "diffusion_models", + "upscale": "upscale_models", "upscale_model": "upscale_models", "upscale_models": "upscale_models", + "upscalemodel": "upscale_models", "upscaler": "upscale_models", "vae": "vae", "vae_approx": "vae_approx", diff --git a/web/resolver/utils/category_utils.js b/web/resolver/utils/category_utils.js index dd89ea5..355be7b 100644 --- a/web/resolver/utils/category_utils.js +++ b/web/resolver/utils/category_utils.js @@ -1,5 +1,22 @@ import { CATEGORY_ALIASES, normalizeCategoryToken } from './category_aliases.generated.js'; +export function normalizeCategoryValue( + category = '', + { + aliases = CATEGORY_ALIASES, + defaultValue = '', + emptyValues = ['unknown', 'none', 'null'], + } = {}, +) { + const token = normalizeCategoryToken(category); + const categoryAliases = aliases && typeof aliases === 'object' + ? aliases + : CATEGORY_ALIASES; + const emptyTokens = new Set(emptyValues); + if (!token || emptyTokens.has(token)) return defaultValue; + return categoryAliases[token] || token; +} + export function normalizeDownloadCategoryValue( category = '', aliases = CATEGORY_ALIASES, diff --git a/web/resolver/views/tabs_image_metadata_methods.js b/web/resolver/views/tabs_image_metadata_methods.js index b4049e8..23b341a 100644 --- a/web/resolver/views/tabs_image_metadata_methods.js +++ b/web/resolver/views/tabs_image_metadata_methods.js @@ -1,28 +1,14 @@ +import { safeStorage } from "../utils/html_utils.js"; +import { + isExistingResolvedModel, + toResolverContextModel, +} from "../node_context_menu.js"; +import { normalizeCategoryValue } from "../utils/category_utils.js"; + const MAX_IMAGE_FILE_SIZE = 64 * 1024 * 1024; const MAX_WORKFLOW_FILE_SIZE = 16 * 1024 * 1024; - -const TRANSFER_CATEGORY_ALIASES = Object.freeze({ - checkpoint: 'checkpoints', - checkpoints: 'checkpoints', - ckpt: 'checkpoints', - lora: 'loras', - loras: 'loras', - clip: 'text_encoders', - text_encoder: 'text_encoders', - text_encoders: 'text_encoders', - vae: 'vae', - control_net: 'controlnet', - controlnet: 'controlnet', - upscale: 'upscale_models', - upscaler: 'upscale_models', - upscale_model: 'upscale_models', - upscale_models: 'upscale_models', - unet: 'diffusion_models', - diffusion_model: 'diffusion_models', - diffusion_models: 'diffusion_models', - embedding: 'embeddings', - embeddings: 'embeddings', -}); +const METADATA_TRANSFER_MODE_STORAGE_KEY = 'model_resolver_metadata_transfer_mode'; +const METADATA_TRANSFER_ACTIVITY_SCOPE_STORAGE_KEY = 'model_resolver_metadata_transfer_activity_scope'; const MERGE_LORA_NODE_TYPES = new Set([ 'LoraLoaderV2', @@ -63,8 +49,7 @@ function isAppendableLoraNodeType(nodeType) { } function normalizeTransferCategory(value) { - const token = String(value || '').trim().toLowerCase().replaceAll('-', '_'); - return TRANSFER_CATEGORY_ALIASES[token] || token; + return normalizeCategoryValue(value); } function getTransferNodeKey(model = {}) { @@ -161,11 +146,23 @@ function buildCivitaiModelData(resources = []) { } export const imageMetadataMethods = { + isMetadataTransferCurrentWorkflow(state = null, workflow = null) { + const transfer = state?.transfer; + if (!transfer) return false; + if (!workflow) return !transfer.workflowSignature; + + const workflowSignature = this.getWorkflowSignature?.(workflow) || ''; + return Boolean(workflowSignature) && transfer.workflowSignature === workflowSignature; + }, + loadImageMetadata() { if (!this.contentElement) return null; this.contentElement.style.overflowY = 'auto'; + const state = getImageInspectorState(this); + const workflow = this.getCurrentWorkflow?.() || null; + const shouldOpenTransfer = !this.isMetadataTransferCurrentWorkflow(state, workflow); this.renderImageMetadataShell(); - void this.openMetadataTransfer(); + if (shouldOpenTransfer) void this.openMetadataTransfer(); return null; }, @@ -244,7 +241,7 @@ export const imageMetadataMethods = { if (action === 'choose-file') fileInput?.click(); if (action === 'clear') this.clearImageMetadata(); if (action === 'clear-transfer-targets') this.clearMetadataTransferTargets(); - if (action === 'retry-transfer-targets') void this.openMetadataTransfer(); + if (action === 'retry-transfer-targets') void this.openMetadataTransfer({ force: true }); if (action === 'apply-transfer') void this.applyMetadataTransfer(); if (action === 'analyze-paste') { const textarea = root.querySelector('.mr-image-inspector-textarea'); @@ -275,14 +272,14 @@ export const imageMetadataMethods = { } if (input.matches?.('[data-image-transfer-mode]')) { const state = getImageInspectorState(this); - if (state.transfer) state.transfer.mode = normalizeTransferMode(input.value); + if (state.transfer) state.transfer.mode = this.setMetadataTransferMode(input.value); this.renderImageMetadataResult(); return; } if (input.matches?.('[data-image-transfer-activity-scope]')) { const state = getImageInspectorState(this); if (state.transfer) { - state.transfer.activityScope = normalizeTransferActivityScope(input.value); + state.transfer.activityScope = this.setMetadataTransferActivityScope(input.value); } this.renderImageMetadataResult(); return; @@ -681,18 +678,22 @@ export const imageMetadataMethods = { }, getMetadataTransferModelInteractionAttrs(model = {}, contextScope = 'loaded_model', label = '') { - const modelPath = String( - model?.resolved_path - || model?.path - || model?.full_path - || '' - ).trim(); - if (model?.exists !== true || !modelPath) return ''; + if (!isExistingResolvedModel(model)) return ''; - const contextModel = { - ...model, - context_scope: contextScope, - }; + let contextModel; + if (contextScope === 'local_model') { + contextModel = toResolverContextModel(model); + } else if ( + contextScope === 'loaded_model' + && typeof this.getLoadedModelContext === 'function' + ) { + contextModel = this.getLoadedModelContext(model); + } else { + contextModel = { + ...model, + context_scope: contextScope, + }; + } const contextMenuAttrs = typeof this.getContextMenuAttrs === 'function' ? this.getContextMenuAttrs(contextModel) : ''; @@ -702,6 +703,27 @@ export const imageMetadataMethods = { return `${contextMenuAttrs}${previewTooltipAttrs}`; }, + getMetadataTransferPreferences() { + return { + mode: normalizeTransferMode(safeStorage.getItem(METADATA_TRANSFER_MODE_STORAGE_KEY)), + activityScope: normalizeTransferActivityScope( + safeStorage.getItem(METADATA_TRANSFER_ACTIVITY_SCOPE_STORAGE_KEY) + ), + }; + }, + + setMetadataTransferMode(value) { + const mode = normalizeTransferMode(value); + safeStorage.setItem(METADATA_TRANSFER_MODE_STORAGE_KEY, mode); + return mode; + }, + + setMetadataTransferActivityScope(value) { + const activityScope = normalizeTransferActivityScope(value); + safeStorage.setItem(METADATA_TRANSFER_ACTIVITY_SCOPE_STORAGE_KEY, activityScope); + return activityScope; + }, + getMetadataTransferSourceSelectionKey(ref = {}) { return String( ref.transferKey @@ -1378,13 +1400,14 @@ export const imageMetadataMethods = { `; }, - async openMetadataTransfer() { + async openMetadataTransfer({ force = false } = {}) { const state = getImageInspectorState(this); const imageRequestToken = state.requestToken; const workflow = this.getCurrentWorkflow?.(); const workflowSignature = workflow ? (this.getWorkflowSignature?.(workflow) || '') : ''; + if (workflow) this.syncWorkflowScopedQueue?.(workflow); const previousTransfer = state.transfer; const preserveSelection = Boolean( workflowSignature @@ -1392,6 +1415,7 @@ export const imageMetadataMethods = { && previousTransfer.workflowSignature === workflowSignature ); const previousSourceSelections = previousTransfer?.sourceSelections; + const savedPreferences = this.getMetadataTransferPreferences(); const sourceSelections = preserveSelection ? previousSourceSelections instanceof Map ? Object.fromEntries(previousSourceSelections) @@ -1405,8 +1429,10 @@ export const imageMetadataMethods = { loading: Boolean(workflow), applying: false, error: '', - mode: normalizeTransferMode(previousTransfer?.mode), - activityScope: normalizeTransferActivityScope(previousTransfer?.activityScope), + mode: normalizeTransferMode(previousTransfer?.mode ?? savedPreferences.mode), + activityScope: normalizeTransferActivityScope( + previousTransfer?.activityScope ?? savedPreferences.activityScope + ), requestToken: token, workflowSignature, targetGroups: [], @@ -1428,18 +1454,30 @@ export const imageMetadataMethods = { } try { - const targetModels = await this.fetchJson( - '/model_resolver/loaded', - { - method: 'POST', - body: JSON.stringify({ workflow }), - }, - 'Scan current workflow for transfer' + const cachedTargetModels = this.getCachedLoadedModelsForSignature?.( + workflowSignature, + { force }, ); + let targetModels = cachedTargetModels; + if (!targetModels) { + targetModels = await this.fetchJson( + '/model_resolver/loaded', + { + method: 'POST', + body: JSON.stringify({ workflow }), + }, + 'Scan current workflow for transfer' + ); + } if ( state.requestToken !== imageRequestToken || state.transfer?.requestToken !== token ) return null; + if (!cachedTargetModels && workflowSignature) { + this.cachedLoadedModelsSignature = workflowSignature; + this.cachedLoadedModelsData = targetModels; + this.saveLoadedModelsCacheForActiveWorkflow?.(); + } state.transfer.loading = false; state.transfer.targetModels = targetModels; state.transfer.targetGroups = this.getMetadataTransferTargetGroups(targetModels); diff --git a/web/resolver/views/tabs_loaded_methods.js b/web/resolver/views/tabs_loaded_methods.js index 429c214..8e6c6f9 100644 --- a/web/resolver/views/tabs_loaded_methods.js +++ b/web/resolver/views/tabs_loaded_methods.js @@ -120,14 +120,13 @@ export const tabsLoadedMethods = { this._loadedModelsLoadToken = loadToken; const workflowSignature = this.getWorkflowSignature(workflow); - if ( - !force && - workflowSignature && - this.cachedLoadedModelsSignature === workflowSignature && - this.cachedLoadedModelsData - ) { + const cachedLoadedModels = this.getCachedLoadedModelsForSignature?.( + workflowSignature, + { force }, + ); + if (cachedLoadedModels) { if (shouldRenderLoadedModels()) { - this.displayLoadedModels(this.contentElement, this.cachedLoadedModelsData); + this.displayLoadedModels(this.contentElement, cachedLoadedModels); } return; }