From 647bda216072a5b1f7d8a36bdc5797be0209b6cd Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sat, 7 Jun 2025 20:22:54 +0800 Subject: [PATCH] Add API endpoint and frontend integration for fetching example image files --- py/routes/misc_routes.py | 146 +++++++++++++++++- .../checkpointModal/ShowcaseView.js | 106 ++++++++++--- static/js/components/checkpointModal/index.js | 57 ++++++- .../js/components/loraModal/ShowcaseView.js | 62 +++++--- static/js/components/loraModal/index.js | 56 ++++++- static/js/utils/uiHelpers.js | 22 +++ 6 files changed, 401 insertions(+), 48 deletions(-) diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index 86059133..953d6666 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -66,6 +66,9 @@ class MiscRoutes: # Add new route for getting trained words app.router.add_get('/api/trained-words', MiscRoutes.get_trained_words) + # Add new route for getting example images folder contents + app.router.add_get('/api/example-image-files', MiscRoutes.get_example_image_files) + @staticmethod async def clear_cache(request): """Clear all cache files from the cache folder""" @@ -462,7 +465,7 @@ class MiscRoutes: model_success = True - for i, image in enumerate(model_images, 1): + for i, image in enumerate(model_images): image_url = image.get('url') if not image_url: continue @@ -479,6 +482,7 @@ class MiscRoutes: logger.debug(f"Skipping unsupported file type: {image_filename}") continue + # Use 0-based indexing instead of 1-based save_filename = f"image_{i}{image_ext}" # If optimizing images and this is a Civitai image, use their pre-optimized WebP version @@ -1118,7 +1122,7 @@ class MiscRoutes: # Handle multiple occurrences of {model} model_count = pattern.count('{model}') - if model_count > 1: + if (model_count > 1): # Replace the first occurrence with a named capture group regex_safe = regex_safe.replace(r'\{model\}', r'(?P.*?)', 1) @@ -1455,3 +1459,141 @@ class MiscRoutes: 'success': False, 'error': str(e) }, status=500) + + @staticmethod + async def get_example_image_files(request): + """ + Get list of example image files for a specific model + + Expects: + - model_hash in query parameters + + Returns: + - List of image files with their paths + """ + try: + # Get the model hash from query parameters + model_hash = request.query.get('model_hash') + + if not model_hash: + return web.json_response({ + 'success': False, + 'error': 'Missing model_hash parameter' + }, status=400) + + # Get the example images path from settings + example_images_path = settings.get('example_images_path') + if not example_images_path: + return web.json_response({ + 'success': False, + 'error': 'No example images path configured' + }, status=400) + + # Construct the folder path for this model + model_folder = os.path.join(example_images_path, model_hash) + + # Check if the folder exists + if not os.path.exists(model_folder): + return web.json_response({ + 'success': False, + 'error': 'No example images found for this model', + 'files': [] + }, status=404) + + # Get list of files in the folder + files = [] + for file in os.listdir(model_folder): + file_path = os.path.join(model_folder, file) + if os.path.isfile(file_path): + # Check if the file is a supported media file + file_ext = os.path.splitext(file)[1].lower() + if (file_ext in SUPPORTED_MEDIA_EXTENSIONS['images'] or + file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos']): + files.append({ + 'name': file, + 'path': f'/example_images_static/{model_hash}/{file}', + 'extension': file_ext, + 'is_video': file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos'] + }) + + # Check if files are using 1-based indexing (looking for pattern like "image_1.jpg") + has_one_based = any(re.match(r'image_1\.\w+$', f['name']) for f in files) + has_zero_based = any(re.match(r'image_0\.\w+$', f['name']) for f in files) + + # If there's 1-based indexing and no 0-based, rename files + if has_one_based and not has_zero_based: + logger.info(f"Converting 1-based to 0-based indexing in {model_folder}") + # Sort files to ensure we process them in the right order + files.sort(key=lambda x: x['name']) + + # First, create a mapping of renames to avoid conflicts + renames = [] + for file in files: + match = re.match(r'image_(\d+)\.(\w+)$', file['name']) + if match: + index = int(match.group(1)) + ext = match.group(2) + if index > 0: # Only rename if index is positive + new_name = f"image_{index-1}.{ext}" + renames.append((file['name'], new_name)) + + # To avoid conflicts, use temporary filenames first + for old_name, new_name in renames: + old_path = os.path.join(model_folder, old_name) + temp_path = os.path.join(model_folder, f"temp_{old_name}") + try: + os.rename(old_path, temp_path) + except Exception as e: + logger.error(f"Failed to rename {old_path} to {temp_path}: {e}") + + # Now rename from temporary names to final names + for old_name, new_name in renames: + temp_path = os.path.join(model_folder, f"temp_{old_name}") + new_path = os.path.join(model_folder, new_name) + try: + os.rename(temp_path, new_path) + logger.debug(f"Renamed {old_name} to {new_name}") + + # Update the entry in our files list + for file in files: + if file['name'] == old_name: + file['name'] = new_name + file['path'] = f'/example_images_static/{model_hash}/{new_name}' + except Exception as e: + logger.error(f"Failed to rename {temp_path} to {new_path}: {e}") + + # Refresh the file list after renaming + files = [] + for file in os.listdir(model_folder): + file_path = os.path.join(model_folder, file) + if os.path.isfile(file_path): + file_ext = os.path.splitext(file)[1].lower() + if (file_ext in SUPPORTED_MEDIA_EXTENSIONS['images'] or + file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos']): + files.append({ + 'name': file, + 'path': f'/example_images_static/{model_hash}/{file}', + 'extension': file_ext, + 'is_video': file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos'] + }) + + # Sort files by their index for consistent ordering + def extract_index(filename): + match = re.match(r'image_(\d+)\.\w+$', filename) + if match: + return int(match.group(1)) + return float('inf') # Put non-matching files at the end + + files.sort(key=lambda x: extract_index(x['name'])) + + return web.json_response({ + 'success': True, + 'files': files + }) + + except Exception as e: + logger.error(f"Failed to get example image files: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/static/js/components/checkpointModal/ShowcaseView.js b/static/js/components/checkpointModal/ShowcaseView.js index 4dbed71d..a5cdfa2f 100644 --- a/static/js/components/checkpointModal/ShowcaseView.js +++ b/static/js/components/checkpointModal/ShowcaseView.js @@ -3,12 +3,6 @@ * Handles showcase content (images, videos) display for checkpoint modal */ import { - showToast, - copyToClipboard, - getLocalExampleImageUrl, - initLazyLoading, - initNsfwBlurHandlers, - initMetadataPanelHandlers, toggleShowcase, setupShowcaseScroll, scrollToTop @@ -20,9 +14,10 @@ import { NSFW_LEVELS } from '../../utils/constants.js'; * Render showcase content * @param {Array} images - Array of images/videos to show * @param {string} modelHash - Model hash for identifying local files + * @param {Array} exampleFiles - Local example files already fetched * @returns {string} HTML content */ -export function renderShowcaseContent(images, modelHash) { +export function renderShowcaseContent(images, exampleFiles = []) { if (!images?.length) return '
No example images available
'; // Filter images based on SFW setting @@ -65,9 +60,85 @@ export function renderShowcaseContent(images, modelHash) { ${hiddenNotification} @@ -205,7 +276,7 @@ function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePrompt, si /** * Generate video wrapper HTML */ -function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel, urls) { +function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl, remoteUrl) { return `
${shouldBlur ? ` @@ -215,10 +286,10 @@ function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metada ` : ''} ${shouldBlur ? ` @@ -237,7 +308,7 @@ function generateVideoWrapper(media, heightPercent, shouldBlur, nsfwText, metada /** * Generate image wrapper HTML */ -function generateImageWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel, urls) { +function generateImageWrapper(media, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl, remoteUrl) { return `
${shouldBlur ? ` @@ -245,9 +316,8 @@ function generateImageWrapper(media, heightPercent, shouldBlur, nsfwText, metada ` : ''} - Preview
- ${renderShowcaseContent(checkpoint.civitai?.images || [], checkpoint.sha256)} +
+ Loading recipes... +
@@ -146,6 +147,56 @@ export function showCheckpointModal(checkpoint) { if (checkpoint.civitai?.modelId && !checkpoint.modelDescription) { loadModelDescription(checkpoint.civitai.modelId, checkpoint.file_path); } + + // Load example images asynchronously + loadExampleImages(checkpoint.civitai?.images, checkpoint.sha256); +} + +/** + * Load example images asynchronously + * @param {Array} images - Array of image objects + * @param {string} modelHash - Model hash for fetching local files + */ +async function loadExampleImages(images, modelHash) { + try { + const showcaseTab = document.getElementById('showcase-tab'); + if (!showcaseTab) return; + + // First fetch local example files + let localFiles = []; + if (modelHash) { + try { + localFiles = await getExampleImageFiles(modelHash); + } catch (error) { + console.error("Failed to get example files:", error); + } + } + + // Then render with both remote images and local files + showcaseTab.innerHTML = renderShowcaseContent(images, localFiles); + + // Re-initialize the showcase event listeners + const carousel = showcaseTab.querySelector('.carousel'); + if (carousel) { + // Only initialize if we actually have examples and they're expanded + if (!carousel.classList.contains('collapsed')) { + initLazyLoading(carousel); + initNsfwBlurHandlers(carousel); + initMetadataPanelHandlers(carousel); + } + } + } catch (error) { + console.error('Error loading example images:', error); + const showcaseTab = document.getElementById('showcase-tab'); + if (showcaseTab) { + showcaseTab.innerHTML = ` +
+ + Error loading example images +
+ `; + } + } } /** diff --git a/static/js/components/loraModal/ShowcaseView.js b/static/js/components/loraModal/ShowcaseView.js index 6c8a7db5..5e2340b1 100644 --- a/static/js/components/loraModal/ShowcaseView.js +++ b/static/js/components/loraModal/ShowcaseView.js @@ -3,12 +3,6 @@ * 处理LoRA模型展示内容(图片、视频)的功能模块 */ import { - showToast, - copyToClipboard, - getLocalExampleImageUrl, - initLazyLoading, - initNsfwBlurHandlers, - initMetadataPanelHandlers, toggleShowcase, setupShowcaseScroll, scrollToTop @@ -17,12 +11,12 @@ import { state } from '../../state/index.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; /** - * 渲染展示内容 + * 获取展示内容并进行渲染 * @param {Array} images - 要展示的图片/视频数组 - * @param {string} modelHash - Model hash for identifying local files - * @returns {string} HTML内容 + * @param {Array} exampleFiles - Local example files already fetched + * @returns {Promise} HTML内容 */ -export function renderShowcaseContent(images, modelHash) { +export function renderShowcaseContent(images, exampleFiles = []) { if (!images?.length) return '
No example images available
'; // Filter images based on SFW setting @@ -65,8 +59,25 @@ export function renderShowcaseContent(images, modelHash) { ${hiddenNotification}
@@ -193,7 +210,7 @@ function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePrompt, si /** * 生成视频包装HTML */ -function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel, urls) { +function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl, remoteUrl) { return `
${shouldBlur ? ` @@ -203,10 +220,10 @@ function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadata ` : ''} ${shouldBlur ? ` @@ -225,7 +242,7 @@ function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadata /** * 生成图片包装HTML */ -function generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel, urls) { +function generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl, remoteUrl) { return `
${shouldBlur ? ` @@ -233,9 +250,8 @@ function generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadata ` : ''} - Preview
- ${renderShowcaseContent(lora.civitai?.images, lora.sha256)} +
+ Loading example images... +
@@ -182,6 +184,56 @@ export function showLoraModal(lora) { // Load recipes for this Lora loadRecipesForLora(lora.model_name, lora.sha256); + + // Load example images asynchronously + loadExampleImages(lora.civitai?.images, lora.sha256); +} + +/** + * Load example images asynchronously + * @param {Array} images - Array of image objects + * @param {string} modelHash - Model hash for fetching local files + */ +async function loadExampleImages(images, modelHash) { + try { + const showcaseTab = document.getElementById('showcase-tab'); + if (!showcaseTab) return; + + // First fetch local example files + let localFiles = []; + if (modelHash) { + try { + localFiles = await getExampleImageFiles(modelHash); + } catch (error) { + console.error("Failed to get example files:", error); + } + } + + // Then render with both remote images and local files + showcaseTab.innerHTML = renderShowcaseContent(images, localFiles); + + // Re-initialize the showcase event listeners + const carousel = showcaseTab.querySelector('.carousel'); + if (carousel) { + // Only initialize if we actually have examples and they're expanded + if (!carousel.classList.contains('collapsed')) { + initLazyLoading(carousel); + initNsfwBlurHandlers(carousel); + initMetadataPanelHandlers(carousel); + } + } + } catch (error) { + console.error('Error loading example images:', error); + const showcaseTab = document.getElementById('showcase-tab'); + if (showcaseTab) { + showcaseTab.innerHTML = ` +
+ + Error loading example images +
+ `; + } + } } // Copy file name function diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 6533ff0b..98de6a5c 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -936,4 +936,26 @@ export function scrollToTop(button) { behavior: 'smooth' }); } +} + +/** + * Get example image files for a specific model from the backend + * @param {string} modelHash - The model's hash + * @returns {Promise} Array of file objects with path and metadata + */ +export async function getExampleImageFiles(modelHash) { + try { + const response = await fetch(`/api/example-image-files?model_hash=${modelHash}`); + const result = await response.json(); + + if (result.success) { + return result.files; + } else { + console.error('Failed to get example image files:', result.error); + return []; + } + } catch (error) { + console.error('Error fetching example image files:', error); + return []; + } } \ No newline at end of file