Implement saving model example images locally. Fixes https://github.com/willmiao/ComfyUI-Lora-Manager/issues/88

This commit is contained in:
Will Miao
2025-04-29 16:18:25 +08:00
parent 4789711910
commit cb876cf77e
20 changed files with 1581 additions and 138 deletions

View File

@@ -6,12 +6,40 @@ import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
import { state } from '../../state/index.js';
import { NSFW_LEVELS } from '../../utils/constants.js';
/**
* Get the local URL for an example image if available
* @param {Object} img - Image object
* @param {number} index - Image index
* @param {string} modelHash - Model hash
* @returns {string|null} - Local URL or null if not available
*/
function getLocalExampleImageUrl(img, index, modelHash) {
if (!modelHash) return null;
// Get remote extension
const remoteExt = (img.url || '').split('?')[0].split('.').pop().toLowerCase();
// If it's a video (mp4), use that extension
if (remoteExt === 'mp4') {
return `/example_images_static/${modelHash}/image_${index + 1}.mp4`;
}
// For images, check if optimization is enabled (defaults to true)
const optimizeImages = state.settings.optimizeExampleImages !== false;
// Use .webp for images if optimization enabled, otherwise use original extension
const extension = optimizeImages ? 'webp' : remoteExt;
return `/example_images_static/${modelHash}/image_${index + 1}.${extension}`;
}
/**
* Render showcase content
* @param {Array} images - Array of images/videos to show
* @param {string} modelHash - Model hash for identifying local files
* @returns {string} HTML content
*/
export function renderShowcaseContent(images) {
export function renderShowcaseContent(images, modelHash) {
if (!images?.length) return '<div class="no-examples">No example images available</div>';
// Filter images based on SFW setting
@@ -53,7 +81,11 @@ export function renderShowcaseContent(images) {
<div class="carousel collapsed">
${hiddenNotification}
<div class="carousel-container">
${filteredImages.map(img => generateMediaWrapper(img)).join('')}
${filteredImages.map((img, index) => {
// Try to get local URL for the example image
const localUrl = getLocalExampleImageUrl(img, index, modelHash);
return generateMediaWrapper(img, localUrl);
}).join('')}
</div>
</div>
`;
@@ -64,7 +96,7 @@ export function renderShowcaseContent(images) {
* @param {Object} media - Media object with image or video data
* @returns {string} HTML content
*/
function generateMediaWrapper(media) {
function generateMediaWrapper(media, localUrl = null) {
// Calculate appropriate aspect ratio:
// 1. Keep original aspect ratio
// 2. Limit maximum height to 60% of viewport height
@@ -117,10 +149,10 @@ function generateMediaWrapper(media) {
// Check if this is a video or image
if (media.type === 'video') {
return generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel);
return generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl);
}
return generateImageWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel);
return generateImageWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl);
}
/**
@@ -193,7 +225,7 @@ function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePrompt, si
/**
* Generate video wrapper HTML
*/
function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel) {
function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl = null) {
return `
<div class="media-wrapper ${shouldBlur ? 'nsfw-media-wrapper' : ''}" style="padding-bottom: ${heightPercent}%">
${shouldBlur ? `
@@ -202,9 +234,11 @@ function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metada
</button>
` : ''}
<video controls autoplay muted loop crossorigin="anonymous"
referrerpolicy="no-referrer" data-src="${media.url}"
referrerpolicy="no-referrer"
data-local-src="${localUrl || ''}"
data-remote-src="${media.url}"
class="lazy ${shouldBlur ? 'blurred' : ''}">
<source data-src="${media.url}" type="video/mp4">
<source data-local-src="${localUrl || ''}" data-remote-src="${media.url}" type="video/mp4">
Your browser does not support video playback
</video>
${shouldBlur ? `
@@ -223,7 +257,7 @@ function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metada
/**
* Generate image wrapper HTML
*/
function generateImageWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel) {
function generateImageWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl = null) {
return `
<div class="media-wrapper ${shouldBlur ? 'nsfw-media-wrapper' : ''}" style="padding-bottom: ${heightPercent}%">
${shouldBlur ? `
@@ -231,7 +265,8 @@ function generateImageWrapper(media, heightPercent, shouldBlur, nsfwText, metada
<i class="fas fa-eye"></i>
</button>
` : ''}
<img data-src="${media.url}"
<img data-local-src="${localUrl || ''}"
data-remote-src="${media.url}"
alt="Preview"
crossorigin="anonymous"
referrerpolicy="no-referrer"
@@ -382,15 +417,73 @@ function initLazyLoading(container) {
const lazyElements = container.querySelectorAll('.lazy');
const lazyLoad = (element) => {
const localSrc = element.dataset.localSrc;
const remoteSrc = element.dataset.remoteSrc;
// Check if element is an image or video
if (element.tagName.toLowerCase() === 'video') {
element.src = element.dataset.src;
element.querySelector('source').src = element.dataset.src;
element.load();
// Try local first, then remote
tryLocalOrFallbackToRemote(element, localSrc, remoteSrc);
} else {
element.src = element.dataset.src;
// For images, we'll use an Image object to test if local file exists
tryLocalImageOrFallbackToRemote(element, localSrc, remoteSrc);
}
element.classList.remove('lazy');
};
// Try to load local image first, fall back to remote if local fails
const tryLocalImageOrFallbackToRemote = (imgElement, localSrc, remoteSrc) => {
// Only try local if we have a local path
if (localSrc) {
const testImg = new Image();
testImg.onload = () => {
// Local image loaded successfully
imgElement.src = localSrc;
};
testImg.onerror = () => {
// Local image failed, use remote
imgElement.src = remoteSrc;
};
// Start loading test image
testImg.src = localSrc;
} else {
// No local path, use remote directly
imgElement.src = remoteSrc;
}
};
// Try to load local video first, fall back to remote if local fails
const tryLocalOrFallbackToRemote = (videoElement, localSrc, remoteSrc) => {
// Only try local if we have a local path
if (localSrc) {
// Try to fetch local file headers to see if it exists
fetch(localSrc, { method: 'HEAD' })
.then(response => {
if (response.ok) {
// Local video exists, use it
videoElement.src = localSrc;
videoElement.querySelector('source').src = localSrc;
} else {
// Local video doesn't exist, use remote
videoElement.src = remoteSrc;
videoElement.querySelector('source').src = remoteSrc;
}
videoElement.load();
})
.catch(() => {
// Error fetching, use remote
videoElement.src = remoteSrc;
videoElement.querySelector('source').src = remoteSrc;
videoElement.load();
});
} else {
// No local path, use remote directly
videoElement.src = remoteSrc;
videoElement.querySelector('source').src = remoteSrc;
videoElement.load();
}
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
@@ -485,4 +578,4 @@ export function scrollToTop(button) {
behavior: 'smooth'
});
}
}
}

View File

@@ -96,7 +96,7 @@ export function showCheckpointModal(checkpoint) {
<div class="tab-content">
<div id="showcase-tab" class="tab-pane active">
${renderShowcaseContent(checkpoint.civitai?.images || [])}
${renderShowcaseContent(checkpoint.civitai?.images || [], checkpoint.sha256)}
</div>
<div id="description-tab" class="tab-pane">