mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-23 22:22:11 -03:00
feat: add checkpoint scanner integration to recipe scanner
- Add CheckpointScanner dependency to RecipeScanner singleton - Implement checkpoint enrichment in recipe data processing - Add _enrich_checkpoint_entry method to enhance checkpoint metadata - Update recipe formatting to include checkpoint information - Extend get_instance, __new__, and __init__ methods to support checkpoint scanner - Add _get_checkpoint_from_version_index method for cache lookup This enables recipe scanner to handle checkpoint models alongside existing LoRA support, providing complete model metadata for recipes.
This commit is contained in:
@@ -588,6 +588,26 @@
|
||||
padding-top: 4px; /* Add padding to prevent first item from being cut off when hovered */
|
||||
}
|
||||
|
||||
.recipe-resources-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.recipe-checkpoint-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.version-divider {
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: var(--space-1) 0;
|
||||
}
|
||||
|
||||
.recipe-lora-item {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
@@ -614,6 +634,13 @@
|
||||
border-left: 4px solid var(--lora-accent);
|
||||
}
|
||||
|
||||
.recipe-lora-item.checkpoint-item {
|
||||
cursor: default;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recipe-lora-item.missing-locally {
|
||||
border-left: 4px solid var(--lora-error);
|
||||
}
|
||||
@@ -962,6 +989,10 @@
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.badge-container .resource-action {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Add styles for missing LoRAs download feature */
|
||||
.recipe-status.missing {
|
||||
position: relative;
|
||||
@@ -1004,3 +1035,61 @@
|
||||
.recipe-status.clickable:hover {
|
||||
background-color: rgba(var(--lora-warning-rgb, 255, 165, 0), 0.2);
|
||||
}
|
||||
|
||||
.recipe-checkpoint-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 0.85em;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.recipe-checkpoint-meta .checkpoint-type {
|
||||
background: var(--lora-surface);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.recipe-resource-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.resource-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.resource-action.compact {
|
||||
padding: 4px 10px;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.resource-action:hover {
|
||||
background: var(--lora-surface);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.resource-action.primary {
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.resource-action.primary:hover {
|
||||
background: color-mix(in oklch, var(--lora-accent), black 10%);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Recipe Modal Component
|
||||
import { showToast, copyToClipboard } from '../utils/uiHelpers.js';
|
||||
import { showToast, copyToClipboard, sendModelPathToWorkflow } from '../utils/uiHelpers.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js';
|
||||
import { updateRecipeMetadata } from '../api/recipeApi.js';
|
||||
import { downloadManager } from '../managers/DownloadManager.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
|
||||
class RecipeModal {
|
||||
constructor() {
|
||||
@@ -339,6 +342,17 @@ class RecipeModal {
|
||||
if (negativePromptElement) promptElement.textContent = 'No negative prompt information available';
|
||||
if (otherParamsElement) otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
|
||||
}
|
||||
|
||||
const checkpointContainer = document.getElementById('recipeCheckpoint');
|
||||
const resourceDivider = document.getElementById('recipeResourceDivider');
|
||||
|
||||
if (checkpointContainer) {
|
||||
checkpointContainer.innerHTML = '';
|
||||
if (recipe.checkpoint && typeof recipe.checkpoint === 'object') {
|
||||
checkpointContainer.innerHTML = this.renderCheckpoint(recipe.checkpoint);
|
||||
this.setupCheckpointActions(checkpointContainer, recipe.checkpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Set LoRAs list and count
|
||||
const lorasListElement = document.getElementById('recipeLorasList');
|
||||
@@ -492,6 +506,12 @@ class RecipeModal {
|
||||
lorasListElement.innerHTML = '<div class="no-loras">No LoRAs associated with this recipe</div>';
|
||||
this.recipeLorasSyntax = '';
|
||||
}
|
||||
|
||||
if (resourceDivider) {
|
||||
const hasCheckpoint = checkpointContainer && checkpointContainer.querySelector('.recipe-lora-item');
|
||||
const hasLoraItems = lorasListElement && lorasListElement.querySelector('.recipe-lora-item');
|
||||
resourceDivider.style.display = hasCheckpoint && hasLoraItems ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
modalManager.showModal('recipeModal');
|
||||
@@ -1047,6 +1067,177 @@ class RecipeModal {
|
||||
}
|
||||
}
|
||||
|
||||
renderCheckpoint(checkpoint) {
|
||||
const existsLocally = !!checkpoint.inLibrary;
|
||||
const localPath = checkpoint.localPath || '';
|
||||
const previewUrl = checkpoint.preview_url || checkpoint.thumbnailUrl || '/loras_static/images/no-preview.png';
|
||||
const isPreviewVideo = typeof previewUrl === 'string' && previewUrl.toLowerCase().endsWith('.mp4');
|
||||
const checkpointName = checkpoint.name || checkpoint.modelName || checkpoint.file_name || 'Checkpoint';
|
||||
const versionLabel = checkpoint.version || checkpoint.modelVersionName || '';
|
||||
const baseModel = checkpoint.baseModel || checkpoint.base_model || '';
|
||||
const modelTypeRaw = (checkpoint.model_type || checkpoint.type || 'checkpoint').toLowerCase();
|
||||
const modelTypeLabel = modelTypeRaw === 'diffusion_model' ? 'Diffusion Model' : 'Checkpoint';
|
||||
|
||||
const previewMedia = isPreviewVideo ? `
|
||||
<video class="thumbnail-video" autoplay loop muted playsinline>
|
||||
<source src="${previewUrl}" type="video/mp4">
|
||||
</video>
|
||||
` : `<img src="${previewUrl}" alt="Checkpoint preview">`;
|
||||
|
||||
const badge = existsLocally ? `
|
||||
<div class="local-badge">
|
||||
<i class="fas fa-check"></i> In Library
|
||||
<div class="local-path">${localPath}</div>
|
||||
</div>
|
||||
` : `
|
||||
<div class="missing-badge">
|
||||
<i class="fas fa-exclamation-triangle"></i> Not in Library
|
||||
</div>
|
||||
`;
|
||||
|
||||
let headerAction = '';
|
||||
if (existsLocally && localPath) {
|
||||
headerAction = `
|
||||
<button class="resource-action primary compact checkpoint-send">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
<span>${translate('recipes.actions.sendCheckpoint', {}, 'Send to ComfyUI')}</span>
|
||||
</button>
|
||||
`;
|
||||
} else if (this.canDownloadCheckpoint(checkpoint)) {
|
||||
headerAction = `
|
||||
<button class="resource-action primary compact checkpoint-download">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>${translate('modals.model.versions.actions.download', {}, 'Download')}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="recipe-lora-item checkpoint-item ${existsLocally ? 'exists-locally' : 'missing-locally'}">
|
||||
<div class="recipe-lora-thumbnail">
|
||||
${previewMedia}
|
||||
</div>
|
||||
<div class="recipe-lora-content">
|
||||
<div class="recipe-lora-header">
|
||||
<h4>${checkpointName}</h4>
|
||||
<div class="badge-container">${headerAction}</div>
|
||||
</div>
|
||||
<div class="recipe-lora-info recipe-checkpoint-meta">
|
||||
${versionLabel ? `<div class="recipe-lora-version">${versionLabel}</div>` : ''}
|
||||
${baseModel ? `<div class="base-model">${baseModel}</div>` : ''}
|
||||
${modelTypeLabel ? `<div class="checkpoint-type">${modelTypeLabel}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupCheckpointActions(container, checkpoint) {
|
||||
const sendBtn = container.querySelector('.checkpoint-send');
|
||||
if (sendBtn) {
|
||||
sendBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.sendCheckpointToWorkflow(checkpoint);
|
||||
});
|
||||
}
|
||||
|
||||
const downloadBtn = container.querySelector('.checkpoint-download');
|
||||
if (downloadBtn) {
|
||||
downloadBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
await this.downloadCheckpoint(checkpoint, downloadBtn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
canDownloadCheckpoint(checkpoint) {
|
||||
if (!checkpoint) return false;
|
||||
const modelId = checkpoint.modelId || checkpoint.modelID || checkpoint.model_id;
|
||||
const versionId = checkpoint.id || checkpoint.modelVersionId;
|
||||
return !!(modelId && versionId);
|
||||
}
|
||||
|
||||
async sendCheckpointToWorkflow(checkpoint) {
|
||||
if (!checkpoint || !checkpoint.localPath) {
|
||||
showToast('toast.recipes.missingCheckpointPath', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const modelType = (checkpoint.model_type || checkpoint.type || 'checkpoint').toLowerCase();
|
||||
const isDiffusionModel = modelType === 'diffusion_model' || modelType === 'unet';
|
||||
const widgetName = isDiffusionModel ? 'unet_name' : 'ckpt_name';
|
||||
|
||||
const actionTypeText = translate(
|
||||
isDiffusionModel ? 'uiHelpers.nodeSelector.diffusionModel' : 'uiHelpers.nodeSelector.checkpoint',
|
||||
{},
|
||||
isDiffusionModel ? 'Diffusion Model' : 'Checkpoint'
|
||||
);
|
||||
const successMessage = translate(
|
||||
isDiffusionModel ? 'uiHelpers.workflow.diffusionModelUpdated' : 'uiHelpers.workflow.checkpointUpdated',
|
||||
{},
|
||||
isDiffusionModel ? 'Diffusion model updated in workflow' : 'Checkpoint updated in workflow'
|
||||
);
|
||||
const failureMessage = translate(
|
||||
isDiffusionModel ? 'uiHelpers.workflow.diffusionModelFailed' : 'uiHelpers.workflow.checkpointFailed',
|
||||
{},
|
||||
isDiffusionModel ? 'Failed to update diffusion model node' : 'Failed to update checkpoint node'
|
||||
);
|
||||
const missingNodesMessage = translate(
|
||||
'uiHelpers.workflow.noMatchingNodes',
|
||||
{},
|
||||
'No compatible nodes available in the current workflow'
|
||||
);
|
||||
const missingTargetMessage = translate(
|
||||
'uiHelpers.workflow.noTargetNodeSelected',
|
||||
{},
|
||||
'No target node selected'
|
||||
);
|
||||
|
||||
await sendModelPathToWorkflow(checkpoint.localPath, {
|
||||
widgetName,
|
||||
collectionType: MODEL_TYPES.CHECKPOINT,
|
||||
actionTypeText,
|
||||
successMessage,
|
||||
failureMessage,
|
||||
missingNodesMessage,
|
||||
missingTargetMessage,
|
||||
});
|
||||
}
|
||||
|
||||
async downloadCheckpoint(checkpoint, button) {
|
||||
if (!this.canDownloadCheckpoint(checkpoint)) {
|
||||
showToast('toast.recipes.missingCheckpointInfo', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const modelId = checkpoint.modelId || checkpoint.modelID || checkpoint.model_id;
|
||||
const versionId = checkpoint.id || checkpoint.modelVersionId;
|
||||
const versionName = checkpoint.version || checkpoint.modelVersionName || checkpoint.name || 'Checkpoint';
|
||||
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await downloadManager.downloadVersionWithDefaults(
|
||||
MODEL_TYPES.CHECKPOINT,
|
||||
modelId,
|
||||
versionId,
|
||||
{
|
||||
versionName,
|
||||
source: 'recipe-modal',
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error downloading checkpoint:', error);
|
||||
showToast('toast.recipes.downloadCheckpointFailed', { message: error.message }, 'error');
|
||||
} finally {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New method to navigate to the LoRAs page
|
||||
navigateToLorasPage(specificLoraIndex = null) {
|
||||
// Close the current modal
|
||||
@@ -1107,4 +1298,4 @@ class RecipeModal {
|
||||
}
|
||||
}
|
||||
|
||||
export { RecipeModal };
|
||||
export { RecipeModal };
|
||||
|
||||
Reference in New Issue
Block a user