mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 22:52:12 -03:00
Add centralized example images setting and update related UI components
This commit is contained in:
@@ -5,6 +5,9 @@ from aiohttp import web
|
|||||||
from ..services.settings_manager import settings
|
from ..services.settings_manager import settings
|
||||||
from ..utils.usage_stats import UsageStats
|
from ..utils.usage_stats import UsageStats
|
||||||
from ..utils.lora_metadata import extract_trained_words
|
from ..utils.lora_metadata import extract_trained_words
|
||||||
|
from ..config import config
|
||||||
|
from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS
|
||||||
|
import re
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -44,6 +47,9 @@ class MiscRoutes:
|
|||||||
|
|
||||||
# Add new route for getting trained words
|
# Add new route for getting trained words
|
||||||
app.router.add_get('/api/trained-words', MiscRoutes.get_trained_words)
|
app.router.add_get('/api/trained-words', MiscRoutes.get_trained_words)
|
||||||
|
|
||||||
|
# Add new route for getting model example files
|
||||||
|
app.router.add_get('/api/model-example-files', MiscRoutes.get_model_example_files)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def clear_cache(request):
|
async def clear_cache(request):
|
||||||
@@ -310,3 +316,90 @@ class MiscRoutes:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_model_example_files(request):
|
||||||
|
"""
|
||||||
|
Get list of example image files for a specific model based on file path
|
||||||
|
|
||||||
|
Expects:
|
||||||
|
- file_path in query parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- List of image files with their paths as static URLs
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the model file path from query parameters
|
||||||
|
file_path = request.query.get('file_path')
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Missing file_path parameter'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Extract directory and base filename
|
||||||
|
model_dir = os.path.dirname(file_path)
|
||||||
|
model_filename = os.path.basename(file_path)
|
||||||
|
model_name = os.path.splitext(model_filename)[0]
|
||||||
|
|
||||||
|
# Check if the directory exists
|
||||||
|
if not os.path.exists(model_dir):
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Model directory not found',
|
||||||
|
'files': []
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
# Look for files matching the pattern modelname.example.<index>.<ext>
|
||||||
|
files = []
|
||||||
|
pattern = f"{model_name}.example."
|
||||||
|
|
||||||
|
for file in os.listdir(model_dir):
|
||||||
|
file_lower = file.lower()
|
||||||
|
if file_lower.startswith(pattern.lower()):
|
||||||
|
file_full_path = os.path.join(model_dir, file)
|
||||||
|
if os.path.isfile(file_full_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']):
|
||||||
|
|
||||||
|
# Extract the index from the filename
|
||||||
|
try:
|
||||||
|
# Extract the part after '.example.' and before file extension
|
||||||
|
index_part = file[len(pattern):].split('.')[0]
|
||||||
|
# Try to parse it as an integer
|
||||||
|
index = int(index_part)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
# If we can't parse the index, use infinity to sort at the end
|
||||||
|
index = float('inf')
|
||||||
|
|
||||||
|
# Convert file path to static URL
|
||||||
|
static_url = config.get_preview_static_url(file_full_path)
|
||||||
|
|
||||||
|
files.append({
|
||||||
|
'name': file,
|
||||||
|
'path': static_url,
|
||||||
|
'extension': file_ext,
|
||||||
|
'is_video': file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos'],
|
||||||
|
'index': index
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort files by their index for consistent ordering
|
||||||
|
files.sort(key=lambda x: x['index'])
|
||||||
|
# Remove the index field as it's only used for sorting
|
||||||
|
for file in files:
|
||||||
|
file.pop('index', None)
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'files': files
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get model example files: {e}", exc_info=True)
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|||||||
@@ -375,6 +375,12 @@ body.modal-open {
|
|||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add disabled style for setting items */
|
||||||
|
.setting-item[data-requires-centralized="true"].disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Control row with label and input together */
|
/* Control row with label and input together */
|
||||||
.setting-row {
|
.setting-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { saveModelMetadata } from '../../api/checkpointApi.js';
|
import { saveModelMetadata } from '../../api/checkpointApi.js';
|
||||||
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
||||||
import { updateCheckpointCard } from '../../utils/cardUpdater.js';
|
import { updateCheckpointCard } from '../../utils/cardUpdater.js';
|
||||||
|
import { state } from '../../state/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the checkpoint modal with the given checkpoint data
|
* Display the checkpoint modal with the given checkpoint data
|
||||||
@@ -149,28 +150,41 @@ export function showCheckpointModal(checkpoint) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load example images asynchronously
|
// Load example images asynchronously
|
||||||
loadExampleImages(checkpoint.civitai?.images, checkpoint.sha256);
|
loadExampleImages(checkpoint.civitai?.images, checkpoint.sha256, checkpoint.file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load example images asynchronously
|
* Load example images asynchronously
|
||||||
* @param {Array} images - Array of image objects
|
* @param {Array} images - Array of image objects
|
||||||
* @param {string} modelHash - Model hash for fetching local files
|
* @param {string} modelHash - Model hash for fetching local files
|
||||||
|
* @param {string} filePath - File path for fetching local files
|
||||||
*/
|
*/
|
||||||
async function loadExampleImages(images, modelHash) {
|
async function loadExampleImages(images, modelHash, filePath) {
|
||||||
try {
|
try {
|
||||||
const showcaseTab = document.getElementById('showcase-tab');
|
const showcaseTab = document.getElementById('showcase-tab');
|
||||||
if (!showcaseTab) return;
|
if (!showcaseTab) return;
|
||||||
|
|
||||||
// First fetch local example files
|
// First fetch local example files
|
||||||
let localFiles = [];
|
let localFiles = [];
|
||||||
if (modelHash) {
|
try {
|
||||||
try {
|
// Choose endpoint based on centralized examples setting
|
||||||
localFiles = await getExampleImageFiles(modelHash);
|
const useCentralized = state.global.settings.useCentralizedExamples !== false;
|
||||||
} catch (error) {
|
const endpoint = useCentralized ? '/api/example-image-files' : '/api/model-example-files';
|
||||||
console.error("Failed to get example files:", error);
|
|
||||||
|
// Use different params based on endpoint
|
||||||
|
const params = useCentralized ?
|
||||||
|
`model_hash=${modelHash}` :
|
||||||
|
`file_path=${encodeURIComponent(filePath)}`;
|
||||||
|
|
||||||
|
const response = await fetch(`${endpoint}?${params}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
localFiles = result.files;
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("Failed to get example files:", error);
|
||||||
|
}
|
||||||
|
|
||||||
// Then render with both remote images and local files
|
// Then render with both remote images and local files
|
||||||
showcaseTab.innerHTML = renderShowcaseContent(images, localFiles);
|
showcaseTab.innerHTML = renderShowcaseContent(images, localFiles);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { saveModelMetadata } from '../../api/loraApi.js';
|
import { saveModelMetadata } from '../../api/loraApi.js';
|
||||||
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
||||||
import { updateLoraCard } from '../../utils/cardUpdater.js';
|
import { updateLoraCard } from '../../utils/cardUpdater.js';
|
||||||
|
import { state } from '../../state/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示LoRA模型弹窗
|
* 显示LoRA模型弹窗
|
||||||
@@ -186,28 +187,42 @@ export function showLoraModal(lora) {
|
|||||||
loadRecipesForLora(lora.model_name, lora.sha256);
|
loadRecipesForLora(lora.model_name, lora.sha256);
|
||||||
|
|
||||||
// Load example images asynchronously
|
// Load example images asynchronously
|
||||||
loadExampleImages(lora.civitai?.images, lora.sha256);
|
loadExampleImages(lora.civitai?.images, lora.sha256, lora.file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load example images asynchronously
|
* Load example images asynchronously
|
||||||
* @param {Array} images - Array of image objects
|
* @param {Array} images - Array of image objects
|
||||||
* @param {string} modelHash - Model hash for fetching local files
|
* @param {string} modelHash - Model hash for fetching local files
|
||||||
|
* @param {string} filePath - File path for fetching local files
|
||||||
*/
|
*/
|
||||||
async function loadExampleImages(images, modelHash) {
|
async function loadExampleImages(images, modelHash, filePath) {
|
||||||
try {
|
try {
|
||||||
const showcaseTab = document.getElementById('showcase-tab');
|
const showcaseTab = document.getElementById('showcase-tab');
|
||||||
if (!showcaseTab) return;
|
if (!showcaseTab) return;
|
||||||
|
|
||||||
// First fetch local example files
|
// First fetch local example files
|
||||||
let localFiles = [];
|
let localFiles = [];
|
||||||
if (modelHash) {
|
|
||||||
try {
|
try {
|
||||||
localFiles = await getExampleImageFiles(modelHash);
|
// Choose endpoint based on centralized examples setting
|
||||||
} catch (error) {
|
const useCentralized = state.global.settings.useCentralizedExamples !== false;
|
||||||
console.error("Failed to get example files:", error);
|
const endpoint = useCentralized ? '/api/example-image-files' : '/api/model-example-files';
|
||||||
|
|
||||||
|
// Use different params based on endpoint
|
||||||
|
const params = useCentralized ?
|
||||||
|
`model_hash=${modelHash}` :
|
||||||
|
`file_path=${encodeURIComponent(filePath)}`;
|
||||||
|
|
||||||
|
const response = await fetch(`${endpoint}?${params}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
localFiles = result.files;
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("Failed to get example files:", error);
|
||||||
|
}
|
||||||
|
|
||||||
// Then render with both remote images and local files
|
// Then render with both remote images and local files
|
||||||
showcaseTab.innerHTML = renderShowcaseContent(images, localFiles);
|
showcaseTab.innerHTML = renderShowcaseContent(images, localFiles);
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ export class SettingsManager {
|
|||||||
state.global.settings.optimizeExampleImages = true;
|
state.global.settings.optimizeExampleImages = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default for useCentralizedExamples if undefined
|
||||||
|
if (state.global.settings.useCentralizedExamples === undefined) {
|
||||||
|
state.global.settings.useCentralizedExamples = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert old boolean compactMode to new displayDensity string
|
// Convert old boolean compactMode to new displayDensity string
|
||||||
if (typeof state.global.settings.displayDensity === 'undefined') {
|
if (typeof state.global.settings.displayDensity === 'undefined') {
|
||||||
if (state.global.settings.compactMode === true) {
|
if (state.global.settings.compactMode === true) {
|
||||||
@@ -109,6 +114,14 @@ export class SettingsManager {
|
|||||||
optimizeExampleImagesCheckbox.checked = state.global.settings.optimizeExampleImages || false;
|
optimizeExampleImagesCheckbox.checked = state.global.settings.optimizeExampleImages || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set centralized examples setting
|
||||||
|
const useCentralizedExamplesCheckbox = document.getElementById('useCentralizedExamples');
|
||||||
|
if (useCentralizedExamplesCheckbox) {
|
||||||
|
useCentralizedExamplesCheckbox.checked = state.global.settings.useCentralizedExamples !== false;
|
||||||
|
// Update dependent controls
|
||||||
|
this.updateExamplesControlsState();
|
||||||
|
}
|
||||||
|
|
||||||
// Load default lora root
|
// Load default lora root
|
||||||
await this.loadLoraRoots();
|
await this.loadLoraRoots();
|
||||||
|
|
||||||
@@ -183,6 +196,10 @@ export class SettingsManager {
|
|||||||
state.global.settings.optimizeExampleImages = value;
|
state.global.settings.optimizeExampleImages = value;
|
||||||
} else if (settingKey === 'compact_mode') {
|
} else if (settingKey === 'compact_mode') {
|
||||||
state.global.settings.compactMode = value;
|
state.global.settings.compactMode = value;
|
||||||
|
} else if (settingKey === 'use_centralized_examples') {
|
||||||
|
state.global.settings.useCentralizedExamples = value;
|
||||||
|
// Update dependent controls state
|
||||||
|
this.updateExamplesControlsState();
|
||||||
} else {
|
} else {
|
||||||
// For any other settings that might be added in the future
|
// For any other settings that might be added in the future
|
||||||
state.global.settings[settingKey] = value;
|
state.global.settings[settingKey] = value;
|
||||||
@@ -193,7 +210,7 @@ export class SettingsManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// For backend settings, make API call
|
// For backend settings, make API call
|
||||||
if (['show_only_sfw', 'blur_mature_content', 'autoplay_on_hover', 'optimize_example_images'].includes(settingKey)) {
|
if (['show_only_sfw', 'blur_mature_content', 'autoplay_on_hover', 'optimize_example_images', 'use_centralized_examples'].includes(settingKey)) {
|
||||||
const payload = {};
|
const payload = {};
|
||||||
payload[settingKey] = value;
|
payload[settingKey] = value;
|
||||||
|
|
||||||
@@ -506,6 +523,42 @@ export class SettingsManager {
|
|||||||
// Add the appropriate density class
|
// Add the appropriate density class
|
||||||
grid.classList.add(`${density}-density`);
|
grid.classList.add(`${density}-density`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply centralized examples toggle state
|
||||||
|
this.updateExamplesControlsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new method to update example control states
|
||||||
|
updateExamplesControlsState() {
|
||||||
|
const useCentralized = state.global.settings.useCentralizedExamples !== false;
|
||||||
|
|
||||||
|
// Find all controls that require centralized mode
|
||||||
|
const exampleSections = document.querySelectorAll('[data-requires-centralized="true"]');
|
||||||
|
exampleSections.forEach(section => {
|
||||||
|
// Enable/disable all inputs and buttons in the section
|
||||||
|
const controls = section.querySelectorAll('input, button, select');
|
||||||
|
controls.forEach(control => {
|
||||||
|
control.disabled = !useCentralized;
|
||||||
|
|
||||||
|
// Add/remove disabled class for styling
|
||||||
|
if (control.classList.contains('primary-btn') || control.classList.contains('secondary-btn')) {
|
||||||
|
if (!useCentralized) {
|
||||||
|
control.classList.add('disabled');
|
||||||
|
} else {
|
||||||
|
control.classList.remove('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Visually show the section as disabled
|
||||||
|
if (!useCentralized) {
|
||||||
|
section.style.opacity = '0.6';
|
||||||
|
section.style.pointerEvents = 'none';
|
||||||
|
} else {
|
||||||
|
section.style.opacity = '';
|
||||||
|
section.style.pointerEvents = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,26 @@
|
|||||||
<h3>Example Images</h3>
|
<h3>Example Images</h3>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label for="useCentralizedExamples">Use Centralized Example Storage</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="useCentralizedExamples" checked
|
||||||
|
onchange="settingsManager.saveToggleSetting('useCentralizedExamples', 'use_centralized_examples')">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-help">
|
||||||
|
When enabled (recommended), example images are stored in a central folder for better organization and performance.
|
||||||
|
When disabled, only example images stored alongside models (e.g., model-name.example.0.jpg) will be shown, but download
|
||||||
|
and management features will be unavailable.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item" data-requires-centralized="true">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<label for="exampleImagesPath">Download Location</label>
|
<label for="exampleImagesPath">Download Location</label>
|
||||||
@@ -273,7 +293,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New migrate section -->
|
<!-- New migrate section -->
|
||||||
<div class="setting-item">
|
<div class="setting-item" data-requires-centralized="true">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<label for="exampleImagesMigratePattern">Migrate Existing Example Images</label>
|
<label for="exampleImagesMigratePattern">Migrate Existing Example Images</label>
|
||||||
@@ -293,7 +313,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item" data-requires-centralized="true">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<label for="optimizeExampleImages">Optimize Downloaded Images</label>
|
<label for="optimizeExampleImages">Optimize Downloaded Images</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user