Add centralized example images setting and update related UI components

This commit is contained in:
Will Miao
2025-06-08 17:38:46 +08:00
parent e872a06f22
commit 4d6ea0236b
6 changed files with 220 additions and 19 deletions

View File

@@ -5,6 +5,9 @@ from aiohttp import web
from ..services.settings_manager import settings
from ..utils.usage_stats import UsageStats
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__)
@@ -44,6 +47,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 model example files
app.router.add_get('/api/model-example-files', MiscRoutes.get_model_example_files)
@staticmethod
async def clear_cache(request):
@@ -310,3 +316,90 @@ class MiscRoutes:
'success': False,
'error': str(e)
}, 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)

View File

@@ -375,6 +375,12 @@ body.modal-open {
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 */
.setting-row {
display: flex;

View File

@@ -15,6 +15,7 @@ import {
import { saveModelMetadata } from '../../api/checkpointApi.js';
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
import { updateCheckpointCard } from '../../utils/cardUpdater.js';
import { state } from '../../state/index.js';
/**
* Display the checkpoint modal with the given checkpoint data
@@ -149,28 +150,41 @@ export function showCheckpointModal(checkpoint) {
}
// Load example images asynchronously
loadExampleImages(checkpoint.civitai?.images, checkpoint.sha256);
loadExampleImages(checkpoint.civitai?.images, checkpoint.sha256, checkpoint.file_path);
}
/**
* Load example images asynchronously
* @param {Array} images - Array of image objects
* @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 {
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);
try {
// Choose endpoint based on centralized examples setting
const useCentralized = state.global.settings.useCentralizedExamples !== false;
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
showcaseTab.innerHTML = renderShowcaseContent(images, localFiles);

View File

@@ -18,6 +18,7 @@ import {
import { saveModelMetadata } from '../../api/loraApi.js';
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
import { updateLoraCard } from '../../utils/cardUpdater.js';
import { state } from '../../state/index.js';
/**
* 显示LoRA模型弹窗
@@ -186,28 +187,42 @@ export function showLoraModal(lora) {
loadRecipesForLora(lora.model_name, lora.sha256);
// Load example images asynchronously
loadExampleImages(lora.civitai?.images, lora.sha256);
loadExampleImages(lora.civitai?.images, lora.sha256, lora.file_path);
}
/**
* Load example images asynchronously
* @param {Array} images - Array of image objects
* @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 {
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);
try {
// Choose endpoint based on centralized examples setting
const useCentralized = state.global.settings.useCentralizedExamples !== false;
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
showcaseTab.innerHTML = renderShowcaseContent(images, localFiles);

View File

@@ -37,6 +37,11 @@ export class SettingsManager {
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
if (typeof state.global.settings.displayDensity === 'undefined') {
if (state.global.settings.compactMode === true) {
@@ -109,6 +114,14 @@ export class SettingsManager {
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
await this.loadLoraRoots();
@@ -183,6 +196,10 @@ export class SettingsManager {
state.global.settings.optimizeExampleImages = value;
} else if (settingKey === 'compact_mode') {
state.global.settings.compactMode = value;
} else if (settingKey === 'use_centralized_examples') {
state.global.settings.useCentralizedExamples = value;
// Update dependent controls state
this.updateExamplesControlsState();
} else {
// For any other settings that might be added in the future
state.global.settings[settingKey] = value;
@@ -193,7 +210,7 @@ export class SettingsManager {
try {
// 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 = {};
payload[settingKey] = value;
@@ -506,6 +523,42 @@ export class SettingsManager {
// Add the appropriate density class
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 = '';
}
});
}
}

View File

@@ -256,6 +256,26 @@
<h3>Example Images</h3>
<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-info">
<label for="exampleImagesPath">Download Location</label>
@@ -273,7 +293,7 @@
</div>
<!-- New migrate section -->
<div class="setting-item">
<div class="setting-item" data-requires-centralized="true">
<div class="setting-row">
<div class="setting-info">
<label for="exampleImagesMigratePattern">Migrate Existing Example Images</label>
@@ -293,7 +313,7 @@
</div>
</div>
<div class="setting-item">
<div class="setting-item" data-requires-centralized="true">
<div class="setting-row">
<div class="setting-info">
<label for="optimizeExampleImages">Optimize Downloaded Images</label>