mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
Implement example image import functionality with UI and backend integration
This commit is contained in:
@@ -2,6 +2,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import re
|
import re
|
||||||
@@ -38,7 +39,7 @@ class ExampleImagesRoutes:
|
|||||||
def setup_routes(app):
|
def setup_routes(app):
|
||||||
"""Register example images routes"""
|
"""Register example images routes"""
|
||||||
app.router.add_post('/api/download-example-images', ExampleImagesRoutes.download_example_images)
|
app.router.add_post('/api/download-example-images', ExampleImagesRoutes.download_example_images)
|
||||||
app.router.add_post('/api/migrate-example-images', ExampleImagesRoutes.migrate_example_images)
|
app.router.add_post('/api/import-example-images', ExampleImagesRoutes.import_example_images)
|
||||||
app.router.add_get('/api/example-images-status', ExampleImagesRoutes.get_example_images_status)
|
app.router.add_get('/api/example-images-status', ExampleImagesRoutes.get_example_images_status)
|
||||||
app.router.add_post('/api/pause-example-images', ExampleImagesRoutes.pause_example_images)
|
app.router.add_post('/api/pause-example-images', ExampleImagesRoutes.pause_example_images)
|
||||||
app.router.add_post('/api/resume-example-images', ExampleImagesRoutes.resume_example_images)
|
app.router.add_post('/api/resume-example-images', ExampleImagesRoutes.resume_example_images)
|
||||||
@@ -781,454 +782,6 @@ class ExampleImagesRoutes:
|
|||||||
# Set download status to not downloading
|
# Set download status to not downloading
|
||||||
is_downloading = False
|
is_downloading = False
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def migrate_example_images(request):
|
|
||||||
"""
|
|
||||||
Migrate existing example images to central storage location
|
|
||||||
|
|
||||||
Expects a JSON body with:
|
|
||||||
{
|
|
||||||
"output_dir": "path/to/output", # Base directory to save example images
|
|
||||||
"pattern": "{model}.example.{index}.{ext}", # Pattern to match example images
|
|
||||||
"optimize": true, # Whether to optimize images (default: true)
|
|
||||||
"model_types": ["lora", "checkpoint"], # Model types to process (default: both)
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
global download_task, is_downloading, download_progress
|
|
||||||
|
|
||||||
if is_downloading:
|
|
||||||
# Create a copy for JSON serialization
|
|
||||||
response_progress = download_progress.copy()
|
|
||||||
response_progress['processed_models'] = list(download_progress['processed_models'])
|
|
||||||
response_progress['refreshed_models'] = list(download_progress['refreshed_models'])
|
|
||||||
|
|
||||||
return web.json_response({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Download or migration already in progress',
|
|
||||||
'status': response_progress
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Parse the request body
|
|
||||||
data = await request.json()
|
|
||||||
output_dir = data.get('output_dir')
|
|
||||||
pattern = data.get('pattern', '{model}.example.{index}.{ext}')
|
|
||||||
optimize = data.get('optimize', True)
|
|
||||||
model_types = data.get('model_types', ['lora', 'checkpoint'])
|
|
||||||
|
|
||||||
if not output_dir:
|
|
||||||
return web.json_response({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Missing output_dir parameter'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# Create the output directory
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Initialize progress tracking
|
|
||||||
download_progress['total'] = 0
|
|
||||||
download_progress['completed'] = 0
|
|
||||||
download_progress['current_model'] = ''
|
|
||||||
download_progress['status'] = 'running'
|
|
||||||
download_progress['errors'] = []
|
|
||||||
download_progress['last_error'] = None
|
|
||||||
download_progress['start_time'] = time.time()
|
|
||||||
download_progress['end_time'] = None
|
|
||||||
download_progress['is_migrating'] = True # Mark this as a migration task
|
|
||||||
|
|
||||||
# Get the processed models list from a file if it exists
|
|
||||||
progress_file = os.path.join(output_dir, '.download_progress.json')
|
|
||||||
if os.path.exists(progress_file):
|
|
||||||
try:
|
|
||||||
with open(progress_file, 'r', encoding='utf-8') as f:
|
|
||||||
saved_progress = json.load(f)
|
|
||||||
download_progress['processed_models'] = set(saved_progress.get('processed_models', []))
|
|
||||||
logger.info(f"Loaded previous progress, {len(download_progress['processed_models'])} models already processed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to load progress file: {e}")
|
|
||||||
download_progress['processed_models'] = set()
|
|
||||||
else:
|
|
||||||
download_progress['processed_models'] = set()
|
|
||||||
|
|
||||||
# Start the migration task
|
|
||||||
is_downloading = True
|
|
||||||
download_task = asyncio.create_task(
|
|
||||||
ExampleImagesRoutes._migrate_all_example_images(
|
|
||||||
output_dir,
|
|
||||||
pattern,
|
|
||||||
optimize,
|
|
||||||
model_types
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a copy for JSON serialization
|
|
||||||
response_progress = download_progress.copy()
|
|
||||||
response_progress['processed_models'] = list(download_progress['processed_models'])
|
|
||||||
response_progress['refreshed_models'] = list(download_progress['refreshed_models'])
|
|
||||||
response_progress['is_migrating'] = True
|
|
||||||
|
|
||||||
return web.json_response({
|
|
||||||
'success': True,
|
|
||||||
'message': 'Migration started',
|
|
||||||
'status': response_progress
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to start example images migration: {e}", exc_info=True)
|
|
||||||
return web.json_response({
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}, status=500)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _migrate_all_example_images(output_dir, pattern, optimize, model_types):
|
|
||||||
"""Migrate example images for all models based on pattern
|
|
||||||
|
|
||||||
Args:
|
|
||||||
output_dir: Base directory to save example images
|
|
||||||
pattern: Pattern to match example images
|
|
||||||
optimize: Whether to optimize images
|
|
||||||
model_types: List of model types to process
|
|
||||||
"""
|
|
||||||
global is_downloading, download_progress
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get the scanners
|
|
||||||
scanners = []
|
|
||||||
if 'lora' in model_types:
|
|
||||||
lora_scanner = await ServiceRegistry.get_lora_scanner()
|
|
||||||
scanners.append(('lora', lora_scanner))
|
|
||||||
|
|
||||||
if 'checkpoint' in model_types:
|
|
||||||
checkpoint_scanner = await ServiceRegistry.get_checkpoint_scanner()
|
|
||||||
scanners.append(('checkpoint', checkpoint_scanner))
|
|
||||||
|
|
||||||
# Convert user pattern to regex
|
|
||||||
regex_pattern = ExampleImagesRoutes._convert_pattern_to_regex(pattern)
|
|
||||||
logger.info(f"Using pattern regex: {regex_pattern.pattern}")
|
|
||||||
|
|
||||||
# Get all models from all scanners
|
|
||||||
all_models = []
|
|
||||||
for scanner_type, scanner in scanners:
|
|
||||||
cache = await scanner.get_cached_data()
|
|
||||||
if cache and cache.raw_data:
|
|
||||||
for model in cache.raw_data:
|
|
||||||
# Only process models with a valid file path and sha256
|
|
||||||
if model.get('file_path') and model.get('sha256'):
|
|
||||||
all_models.append((scanner_type, model, scanner))
|
|
||||||
|
|
||||||
# Update total count
|
|
||||||
download_progress['total'] = len(all_models)
|
|
||||||
logger.info(f"Found {download_progress['total']} models to check for example images")
|
|
||||||
|
|
||||||
# Process each model
|
|
||||||
for scanner_type, model, scanner in all_models:
|
|
||||||
# Check if download is paused
|
|
||||||
while download_progress['status'] == 'paused':
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
# Check if download should continue
|
|
||||||
if download_progress['status'] != 'running':
|
|
||||||
logger.info(f"Migration stopped: {download_progress['status']}")
|
|
||||||
break
|
|
||||||
|
|
||||||
model_hash = model.get('sha256', '').lower()
|
|
||||||
model_name = model.get('model_name', 'Unknown')
|
|
||||||
model_file_path = model.get('file_path', '')
|
|
||||||
model_file_name = os.path.basename(model_file_path) if model_file_path else ''
|
|
||||||
model_dir_path = os.path.dirname(model_file_path) if model_file_path else ''
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Update current model info
|
|
||||||
download_progress['current_model'] = f"{model_name} ({model_hash[:8]})"
|
|
||||||
|
|
||||||
# Skip if already processed
|
|
||||||
if model_hash in download_progress['processed_models']:
|
|
||||||
logger.debug(f"Skipping already processed model: {model_name}")
|
|
||||||
download_progress['completed'] += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Find matching example files based on pattern
|
|
||||||
if model_file_name and os.path.exists(model_dir_path):
|
|
||||||
example_files = ExampleImagesRoutes._find_matching_example_files(
|
|
||||||
model_dir_path,
|
|
||||||
model_file_name,
|
|
||||||
regex_pattern
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process found files
|
|
||||||
if example_files:
|
|
||||||
logger.info(f"Found {len(example_files)} example images for {model_name}")
|
|
||||||
|
|
||||||
# Create model directory in output location
|
|
||||||
model_dir = os.path.join(output_dir, model_hash)
|
|
||||||
os.makedirs(model_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Track local image paths for metadata update
|
|
||||||
local_image_paths = []
|
|
||||||
|
|
||||||
# Migrate each example file
|
|
||||||
for local_image_path, index in example_files:
|
|
||||||
# Get file extension
|
|
||||||
local_ext = os.path.splitext(local_image_path)[1].lower()
|
|
||||||
save_filename = f"image_{index}{local_ext}"
|
|
||||||
save_path = os.path.join(model_dir, save_filename)
|
|
||||||
|
|
||||||
# Track all local image paths for potential metadata update
|
|
||||||
local_image_paths.append(local_image_path)
|
|
||||||
|
|
||||||
# Skip if already exists in output directory
|
|
||||||
if os.path.exists(save_path):
|
|
||||||
logger.debug(f"File already exists in output: {save_path}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Copy the file
|
|
||||||
with open(local_image_path, 'rb') as src_file:
|
|
||||||
with open(save_path, 'wb') as dst_file:
|
|
||||||
dst_file.write(src_file.read())
|
|
||||||
logger.debug(f"Migrated {os.path.basename(local_image_path)} to {save_path}")
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Failed to copy file {os.path.basename(local_image_path)}: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
download_progress['errors'].append(error_msg)
|
|
||||||
download_progress['last_error'] = error_msg
|
|
||||||
|
|
||||||
# Update model metadata if local images were found
|
|
||||||
if local_image_paths:
|
|
||||||
await ExampleImagesRoutes._update_model_metadata_from_local_examples(
|
|
||||||
model,
|
|
||||||
local_image_paths,
|
|
||||||
scanner_type,
|
|
||||||
scanner
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mark this model as processed
|
|
||||||
download_progress['processed_models'].add(model_hash)
|
|
||||||
|
|
||||||
# Save progress to file periodically
|
|
||||||
if download_progress['completed'] % 10 == 0 or download_progress['completed'] == download_progress['total'] - 1:
|
|
||||||
progress_file = os.path.join(output_dir, '.download_progress.json')
|
|
||||||
with open(progress_file, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump({
|
|
||||||
'processed_models': list(download_progress['processed_models']),
|
|
||||||
'refreshed_models': list(download_progress['refreshed_models']),
|
|
||||||
'completed': download_progress['completed'],
|
|
||||||
'total': download_progress['total'],
|
|
||||||
'last_update': time.time()
|
|
||||||
}, f, indent=2)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Error processing model {model.get('model_name')}: {str(e)}"
|
|
||||||
logger.error(error_msg, exc_info=True)
|
|
||||||
download_progress['errors'].append(error_msg)
|
|
||||||
download_progress['last_error'] = error_msg
|
|
||||||
|
|
||||||
# Update progress
|
|
||||||
download_progress['completed'] += 1
|
|
||||||
|
|
||||||
# Mark as completed
|
|
||||||
download_progress['status'] = 'completed'
|
|
||||||
download_progress['end_time'] = time.time()
|
|
||||||
download_progress['is_migrating'] = False
|
|
||||||
logger.info(f"Example images migration completed: {download_progress['completed']}/{download_progress['total']} models processed")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Error during example images migration: {str(e)}"
|
|
||||||
logger.error(error_msg, exc_info=True)
|
|
||||||
download_progress['errors'].append(error_msg)
|
|
||||||
download_progress['last_error'] = error_msg
|
|
||||||
download_progress['status'] = 'error'
|
|
||||||
download_progress['end_time'] = time.time()
|
|
||||||
download_progress['is_migrating'] = False
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Save final progress to file
|
|
||||||
try:
|
|
||||||
progress_file = os.path.join(output_dir, '.download_progress.json')
|
|
||||||
with open(progress_file, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump({
|
|
||||||
'processed_models': list(download_progress['processed_models']),
|
|
||||||
'refreshed_models': list(download_progress['refreshed_models']),
|
|
||||||
'completed': download_progress['completed'],
|
|
||||||
'total': download_progress['total'],
|
|
||||||
'last_update': time.time(),
|
|
||||||
'status': download_progress['status'],
|
|
||||||
'is_migrating': False
|
|
||||||
}, f, indent=2)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to save progress file: {e}")
|
|
||||||
|
|
||||||
# Set download status to not downloading
|
|
||||||
is_downloading = False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _convert_pattern_to_regex(pattern):
|
|
||||||
"""Convert a user-friendly template pattern to a regex pattern
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pattern: Template pattern string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
re.Pattern: Compiled regex pattern object
|
|
||||||
"""
|
|
||||||
# Normalize path separators to forward slashes for consistent matching
|
|
||||||
pattern = pattern.replace('\\', '/')
|
|
||||||
|
|
||||||
# Escape special regex characters
|
|
||||||
regex_safe = re.escape(pattern)
|
|
||||||
|
|
||||||
# Handle multiple occurrences of {model}
|
|
||||||
model_count = pattern.count('{model}')
|
|
||||||
if (model_count > 1):
|
|
||||||
# Replace the first occurrence with a named capture group
|
|
||||||
regex_safe = regex_safe.replace(r'\{model\}', r'(?P<model>.*?)', 1)
|
|
||||||
|
|
||||||
# Replace subsequent occurrences with a back-reference
|
|
||||||
# Using (?P=model) for Python's regex named backreference syntax
|
|
||||||
for _ in range(model_count - 1):
|
|
||||||
regex_safe = regex_safe.replace(r'\{model\}', r'(?P=model)', 1)
|
|
||||||
else:
|
|
||||||
# Just one occurrence, handle normally
|
|
||||||
regex_safe = regex_safe.replace(r'\{model\}', r'(?P<model>.*?)')
|
|
||||||
|
|
||||||
# {index} becomes a capture group for digits
|
|
||||||
regex_safe = regex_safe.replace(r'\{index\}', r'(?P<index>\d+)')
|
|
||||||
|
|
||||||
# {ext} becomes a capture group for file extension WITHOUT including the dot
|
|
||||||
regex_safe = regex_safe.replace(r'\{ext\}', r'(?P<ext>\w+)')
|
|
||||||
|
|
||||||
# Handle wildcard * character (which was escaped earlier)
|
|
||||||
regex_safe = regex_safe.replace(r'\*', r'.*?')
|
|
||||||
|
|
||||||
logger.info(f"Converted pattern '{pattern}' to regex: '{regex_safe}'")
|
|
||||||
|
|
||||||
# Compile the regex pattern
|
|
||||||
return re.compile(regex_safe)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _find_matching_example_files(dir_path, model_filename, regex_pattern):
|
|
||||||
"""Find example files matching the pattern in the given directory
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dir_path: Directory to search in
|
|
||||||
model_filename: Model filename (without extension)
|
|
||||||
regex_pattern: Compiled regex pattern to match against
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: List of tuples (file_path, index) of matching files
|
|
||||||
"""
|
|
||||||
matching_files = []
|
|
||||||
model_name = os.path.splitext(model_filename)[0]
|
|
||||||
|
|
||||||
# Check if pattern contains a directory separator
|
|
||||||
has_subdirs = '/' in regex_pattern.pattern or '\\\\' in regex_pattern.pattern
|
|
||||||
|
|
||||||
# Determine search paths (keep existing logic for subdirectories)
|
|
||||||
if has_subdirs:
|
|
||||||
# Handle patterns with subdirectories
|
|
||||||
subdir_match = re.match(r'.*(?P<model>.*?)(/|\\\\).*', regex_pattern.pattern)
|
|
||||||
if subdir_match:
|
|
||||||
potential_subdir = os.path.join(dir_path, model_name)
|
|
||||||
if os.path.exists(potential_subdir) and os.path.isdir(potential_subdir):
|
|
||||||
search_paths = [potential_subdir]
|
|
||||||
else:
|
|
||||||
search_paths = [dir_path]
|
|
||||||
else:
|
|
||||||
search_paths = [dir_path]
|
|
||||||
else:
|
|
||||||
search_paths = [dir_path]
|
|
||||||
|
|
||||||
for search_path in search_paths:
|
|
||||||
if not os.path.exists(search_path):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# For optimized performance: create a model name prefix check
|
|
||||||
# This works for any pattern where the model name appears at the start
|
|
||||||
if not has_subdirs:
|
|
||||||
# Get list of all files first
|
|
||||||
all_files = os.listdir(search_path)
|
|
||||||
|
|
||||||
# First pass: filter files that start with model name (case insensitive)
|
|
||||||
# This is much faster than regex for initial filtering
|
|
||||||
potential_matches = []
|
|
||||||
lower_model_name = model_name.lower()
|
|
||||||
|
|
||||||
for file in all_files:
|
|
||||||
# Quick check if file starts with model name
|
|
||||||
if file.lower().startswith(lower_model_name):
|
|
||||||
file_path = os.path.join(search_path, file)
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
potential_matches.append((file, file_path))
|
|
||||||
|
|
||||||
# Second pass: apply full regex only to potential matches
|
|
||||||
for file, file_path in potential_matches:
|
|
||||||
match = regex_pattern.match(file)
|
|
||||||
if match:
|
|
||||||
# Verify model name matches exactly what we're looking for
|
|
||||||
if match.group('model') != model_name:
|
|
||||||
logger.debug(f"File {file} matched pattern but model name {match.group('model')} doesn't match {model_name}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if file extension is supported
|
|
||||||
file_ext = os.path.splitext(file)[1].lower()
|
|
||||||
is_supported = (file_ext in SUPPORTED_MEDIA_EXTENSIONS['images'] or
|
|
||||||
file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos'])
|
|
||||||
|
|
||||||
if is_supported:
|
|
||||||
# Extract index from match
|
|
||||||
try:
|
|
||||||
index = int(match.group('index'))
|
|
||||||
except (IndexError, ValueError):
|
|
||||||
index = len(matching_files) + 1
|
|
||||||
|
|
||||||
matching_files.append((file_path, index))
|
|
||||||
else:
|
|
||||||
# Original scanning logic for patterns with subdirectories
|
|
||||||
for file in os.listdir(search_path):
|
|
||||||
file_path = os.path.join(search_path, file)
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
# Try to match the filename directly first
|
|
||||||
match = regex_pattern.match(file)
|
|
||||||
|
|
||||||
# If no match and subdirs are expected, try the relative path
|
|
||||||
if not match and has_subdirs:
|
|
||||||
# Get relative path and normalize slashes for consistent matching
|
|
||||||
rel_path = os.path.relpath(file_path, dir_path)
|
|
||||||
# Replace Windows backslashes with forward slashes for consistent regex matching
|
|
||||||
rel_path = rel_path.replace('\\', '/')
|
|
||||||
match = regex_pattern.match(rel_path)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
# For subdirectory patterns, model name in the match might refer to the dir name only
|
|
||||||
# so we need a different checking logic
|
|
||||||
matched_model = match.group('model')
|
|
||||||
if has_subdirs and '/' in rel_path:
|
|
||||||
# For subdirectory patterns, it's okay if just the folder name matches
|
|
||||||
folder_name = rel_path.split('/')[0]
|
|
||||||
if matched_model != model_name and matched_model != folder_name:
|
|
||||||
logger.debug(f"File {file} matched pattern but model name {matched_model} doesn't match {model_name}")
|
|
||||||
continue
|
|
||||||
elif matched_model != model_name:
|
|
||||||
logger.debug(f"File {file} matched pattern but model name {matched_model} doesn't match {model_name}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
file_ext = os.path.splitext(file)[1].lower()
|
|
||||||
is_supported = (file_ext in SUPPORTED_MEDIA_EXTENSIONS['images'] or
|
|
||||||
file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos'])
|
|
||||||
|
|
||||||
if is_supported:
|
|
||||||
try:
|
|
||||||
index = int(match.group('index'))
|
|
||||||
except (IndexError, ValueError):
|
|
||||||
index = len(matching_files) + 1
|
|
||||||
|
|
||||||
matching_files.append((file_path, index))
|
|
||||||
|
|
||||||
# Sort files by their index
|
|
||||||
matching_files.sort(key=lambda x: x[1])
|
|
||||||
return matching_files
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def open_example_images_folder(request):
|
async def open_example_images_folder(request):
|
||||||
"""
|
"""
|
||||||
@@ -1426,3 +979,269 @@ class ExampleImagesRoutes:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def import_example_images(request):
|
||||||
|
"""
|
||||||
|
Import local example images for a model
|
||||||
|
|
||||||
|
Expects:
|
||||||
|
- multipart/form-data with model_hash and files fields
|
||||||
|
OR
|
||||||
|
- JSON request with model_hash and file_paths
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Success status and list of imported files
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
model_hash = None
|
||||||
|
files_to_import = []
|
||||||
|
temp_files_to_cleanup = []
|
||||||
|
|
||||||
|
# Check if this is a multipart form data request (direct file upload)
|
||||||
|
if request.content_type and 'multipart/form-data' in request.content_type:
|
||||||
|
reader = await request.multipart()
|
||||||
|
|
||||||
|
# First, get the model_hash
|
||||||
|
field = await reader.next()
|
||||||
|
if field.name == 'model_hash':
|
||||||
|
model_hash = await field.text()
|
||||||
|
|
||||||
|
# Then process all files
|
||||||
|
while True:
|
||||||
|
field = await reader.next()
|
||||||
|
if field is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
if field.name == 'files':
|
||||||
|
# Create a temporary file with a proper suffix for type detection
|
||||||
|
file_name = field.filename
|
||||||
|
file_ext = os.path.splitext(file_name)[1].lower()
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=file_ext, delete=False) as tmp_file:
|
||||||
|
temp_path = tmp_file.name
|
||||||
|
temp_files_to_cleanup.append(temp_path) # Track for cleanup
|
||||||
|
|
||||||
|
# Write chunks to the temp file
|
||||||
|
while True:
|
||||||
|
chunk = await field.read_chunk()
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
tmp_file.write(chunk)
|
||||||
|
|
||||||
|
# Add to our list of files to process
|
||||||
|
files_to_import.append(temp_path)
|
||||||
|
else:
|
||||||
|
# Parse JSON request (legacy method with file paths)
|
||||||
|
data = await request.json()
|
||||||
|
model_hash = data.get('model_hash')
|
||||||
|
files_to_import = data.get('file_paths', [])
|
||||||
|
|
||||||
|
if not model_hash:
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Missing model_hash parameter'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
if not files_to_import:
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': 'No files provided to import'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Get example images path
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Find the model and get current metadata
|
||||||
|
lora_scanner = await ServiceRegistry.get_lora_scanner()
|
||||||
|
checkpoint_scanner = await ServiceRegistry.get_checkpoint_scanner()
|
||||||
|
|
||||||
|
model_data = None
|
||||||
|
scanner = None
|
||||||
|
|
||||||
|
# Check both scanners to find the model
|
||||||
|
for scan_obj in [lora_scanner, checkpoint_scanner]:
|
||||||
|
cache = await scan_obj.get_cached_data()
|
||||||
|
for item in cache.raw_data:
|
||||||
|
if item.get('sha256') == model_hash:
|
||||||
|
model_data = item
|
||||||
|
scanner = scan_obj
|
||||||
|
break
|
||||||
|
if model_data:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not model_data:
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': f"Model with hash {model_hash} not found in cache"
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
# Get current number of images in civitai.images array
|
||||||
|
civitai_data = model_data.get('civitai')
|
||||||
|
current_images = civitai_data.get('images', []) if civitai_data is not None else []
|
||||||
|
next_index = len(current_images)
|
||||||
|
|
||||||
|
# Create model folder
|
||||||
|
model_folder = os.path.join(example_images_path, model_hash)
|
||||||
|
os.makedirs(model_folder, exist_ok=True)
|
||||||
|
|
||||||
|
imported_files = []
|
||||||
|
errors = []
|
||||||
|
newly_imported_paths = []
|
||||||
|
|
||||||
|
# Process each file path
|
||||||
|
for file_path in files_to_import:
|
||||||
|
try:
|
||||||
|
# Ensure file exists
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
errors.append(f"File not found: {file_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if file type is supported
|
||||||
|
file_ext = os.path.splitext(file_path)[1].lower()
|
||||||
|
if not (file_ext in SUPPORTED_MEDIA_EXTENSIONS['images'] or
|
||||||
|
file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos']):
|
||||||
|
errors.append(f"Unsupported file type: {file_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Generate new filename with sequential index starting from current images length
|
||||||
|
new_filename = f"image_{next_index}{file_ext}"
|
||||||
|
next_index += 1
|
||||||
|
|
||||||
|
dest_path = os.path.join(model_folder, new_filename)
|
||||||
|
|
||||||
|
# Copy the file
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(file_path, dest_path)
|
||||||
|
newly_imported_paths.append(dest_path)
|
||||||
|
|
||||||
|
# Add to imported files list
|
||||||
|
imported_files.append({
|
||||||
|
'name': new_filename,
|
||||||
|
'path': f'/example_images_static/{model_hash}/{new_filename}',
|
||||||
|
'extension': file_ext,
|
||||||
|
'is_video': file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos']
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Error importing {file_path}: {str(e)}")
|
||||||
|
|
||||||
|
# Update metadata with new example images
|
||||||
|
updated_images = await ExampleImagesRoutes._update_metadata_after_import(
|
||||||
|
model_hash,
|
||||||
|
model_data,
|
||||||
|
scanner,
|
||||||
|
newly_imported_paths
|
||||||
|
)
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': len(imported_files) > 0,
|
||||||
|
'message': f'Successfully imported {len(imported_files)} files' +
|
||||||
|
(f' with {len(errors)} errors' if errors else ''),
|
||||||
|
'files': imported_files,
|
||||||
|
'errors': errors,
|
||||||
|
'updated_images': updated_images,
|
||||||
|
"model_file_path": model_data.get('file_path', ''),
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to import example images: {e}", exc_info=True)
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
finally:
|
||||||
|
# Clean up temporary files if any
|
||||||
|
for temp_file in temp_files_to_cleanup:
|
||||||
|
try:
|
||||||
|
os.remove(temp_file)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to remove temporary file {temp_file}: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _update_metadata_after_import(model_hash, model_data, scanner, newly_imported_paths):
|
||||||
|
"""
|
||||||
|
Update model metadata after importing example images by appending new images to the existing array
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_hash: SHA256 hash of the model
|
||||||
|
model_data: Model data dictionary
|
||||||
|
scanner: Scanner instance (lora or checkpoint)
|
||||||
|
newly_imported_paths: List of paths to newly imported files
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Updated images array
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Ensure civitai field exists in model data
|
||||||
|
if not model_data.get('civitai'):
|
||||||
|
model_data['civitai'] = {}
|
||||||
|
|
||||||
|
# Ensure images array exists
|
||||||
|
if not model_data['civitai'].get('images'):
|
||||||
|
model_data['civitai']['images'] = []
|
||||||
|
|
||||||
|
# Get current images array
|
||||||
|
images = model_data['civitai']['images']
|
||||||
|
|
||||||
|
# Add new image entries for each imported file
|
||||||
|
for path in newly_imported_paths:
|
||||||
|
# Determine if it's a video or image
|
||||||
|
file_ext = os.path.splitext(path)[1].lower()
|
||||||
|
is_video = file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos']
|
||||||
|
|
||||||
|
# Create image metadata entry
|
||||||
|
image_entry = {
|
||||||
|
"url": "", # Empty URL as requested
|
||||||
|
"nsfwLevel": 0,
|
||||||
|
"width": 720, # Default dimensions
|
||||||
|
"height": 1280,
|
||||||
|
"type": "video" if is_video else "image",
|
||||||
|
"meta": None,
|
||||||
|
"hasMeta": False,
|
||||||
|
"hasPositivePrompt": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to get actual dimensions if it's an image
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
if not is_video and os.path.exists(path):
|
||||||
|
with Image.open(path) as img:
|
||||||
|
image_entry["width"], image_entry["height"] = img.size
|
||||||
|
except:
|
||||||
|
# If PIL fails or isn't available, use default dimensions
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Append to the existing images array
|
||||||
|
images.append(image_entry)
|
||||||
|
|
||||||
|
# Save metadata to the .metadata.json file
|
||||||
|
file_path = model_data.get('file_path')
|
||||||
|
if file_path:
|
||||||
|
base_path = os.path.splitext(file_path)[0]
|
||||||
|
metadata_path = f"{base_path}.metadata.json"
|
||||||
|
try:
|
||||||
|
# Create a copy of the model data without the 'folder' field
|
||||||
|
model_copy = model_data.copy()
|
||||||
|
model_copy.pop('folder', None)
|
||||||
|
|
||||||
|
# Write the metadata to file
|
||||||
|
with open(metadata_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(model_copy, f, indent=2, ensure_ascii=False)
|
||||||
|
logger.info(f"Saved metadata to {metadata_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save metadata to {metadata_path}: {str(e)}")
|
||||||
|
|
||||||
|
# Save updated metadata to scanner cache
|
||||||
|
if file_path:
|
||||||
|
await scanner.update_single_model_cache(file_path, file_path, model_data)
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update metadata after import: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
@@ -290,3 +290,94 @@
|
|||||||
.lazy[src] {
|
.lazy[src] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Example Import Area */
|
||||||
|
.example-import-area {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-import-area.empty {
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
padding: var(--space-4) var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-container {
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: var(--space-4);
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: var(--lora-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-container.highlight {
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-placeholder i {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
/* color: var(--lora-accent); */
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-placeholder h3 {
|
||||||
|
margin: 0 0 var(--space-1);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-placeholder p {
|
||||||
|
margin: var(--space-1) 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-placeholder .sub-text {
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin: var(--space-1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-formats {
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
opacity: 0.6 !important;
|
||||||
|
margin-top: var(--space-2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-files-btn {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: var(--lora-text);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-files-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For dark theme */
|
||||||
|
[data-theme="dark"] .import-container {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
@@ -17,7 +17,10 @@ import { NSFW_LEVELS } from '../../utils/constants.js';
|
|||||||
* @returns {Promise<string>} HTML内容
|
* @returns {Promise<string>} HTML内容
|
||||||
*/
|
*/
|
||||||
export function renderShowcaseContent(images, exampleFiles = []) {
|
export function renderShowcaseContent(images, exampleFiles = []) {
|
||||||
if (!images?.length) return '<div class="no-examples">No example images available</div>';
|
if (!images?.length) {
|
||||||
|
// Replace empty message with import interface
|
||||||
|
return renderImportInterface(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Filter images based on SFW setting
|
// Filter images based on SFW setting
|
||||||
const showOnlySFW = state.settings.show_only_sfw;
|
const showOnlySFW = state.settings.show_only_sfw;
|
||||||
@@ -136,10 +139,202 @@ export function renderShowcaseContent(images, exampleFiles = []) {
|
|||||||
);
|
);
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add import interface at the bottom of existing examples -->
|
||||||
|
${renderImportInterface(false)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the import interface for example images
|
||||||
|
* @param {boolean} isEmpty - Whether there are no existing examples
|
||||||
|
* @returns {string} HTML content for import interface
|
||||||
|
*/
|
||||||
|
function renderImportInterface(isEmpty) {
|
||||||
|
return `
|
||||||
|
<div class="example-import-area ${isEmpty ? 'empty' : ''}">
|
||||||
|
<div class="import-container" id="exampleImportContainer">
|
||||||
|
<div class="import-placeholder">
|
||||||
|
<i class="fas fa-cloud-upload-alt"></i>
|
||||||
|
<h3>${isEmpty ? 'No example images available' : 'Add more examples'}</h3>
|
||||||
|
<p>Drag & drop images or videos here</p>
|
||||||
|
<p class="sub-text">or</p>
|
||||||
|
<button class="select-files-btn" id="selectExampleFilesBtn">
|
||||||
|
<i class="fas fa-folder-open"></i> Select Files
|
||||||
|
</button>
|
||||||
|
<p class="import-formats">Supported formats: jpg, png, gif, webp, mp4, webm</p>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="exampleFilesInput" multiple accept="image/*,video/mp4,video/webm" style="display: none;">
|
||||||
|
<div class="import-progress-container" style="display: none;">
|
||||||
|
<div class="import-progress">
|
||||||
|
<div class="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-text">Importing files...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the import functionality for example images
|
||||||
|
* @param {string} modelHash - The SHA256 hash of the model
|
||||||
|
* @param {Element} container - The container element for the import area
|
||||||
|
*/
|
||||||
|
export function initExampleImport(modelHash, container) {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const importContainer = container.querySelector('#exampleImportContainer');
|
||||||
|
const fileInput = container.querySelector('#exampleFilesInput');
|
||||||
|
const selectFilesBtn = container.querySelector('#selectExampleFilesBtn');
|
||||||
|
|
||||||
|
// Set up file selection button
|
||||||
|
if (selectFilesBtn) {
|
||||||
|
selectFilesBtn.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file selection
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
handleImportFiles(Array.from(e.target.files), modelHash, importContainer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up drag and drop
|
||||||
|
if (importContainer) {
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||||
|
importContainer.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function preventDefaults(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight drop area on drag over
|
||||||
|
['dragenter', 'dragover'].forEach(eventName => {
|
||||||
|
importContainer.addEventListener(eventName, () => {
|
||||||
|
importContainer.classList.add('highlight');
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove highlight on drag leave
|
||||||
|
['dragleave', 'drop'].forEach(eventName => {
|
||||||
|
importContainer.addEventListener(eventName, () => {
|
||||||
|
importContainer.classList.remove('highlight');
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle dropped files
|
||||||
|
importContainer.addEventListener('drop', (e) => {
|
||||||
|
const files = Array.from(e.dataTransfer.files);
|
||||||
|
handleImportFiles(files, modelHash, importContainer);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the file import process
|
||||||
|
* @param {File[]} files - Array of files to import
|
||||||
|
* @param {string} modelHash - The SHA256 hash of the model
|
||||||
|
* @param {Element} importContainer - The container element for import UI
|
||||||
|
*/
|
||||||
|
async function handleImportFiles(files, modelHash, importContainer) {
|
||||||
|
// Filter for supported file types
|
||||||
|
const supportedImages = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||||
|
const supportedVideos = ['.mp4', '.webm'];
|
||||||
|
const supportedExtensions = [...supportedImages, ...supportedVideos];
|
||||||
|
|
||||||
|
const validFiles = files.filter(file => {
|
||||||
|
const ext = '.' + file.name.split('.').pop().toLowerCase();
|
||||||
|
return supportedExtensions.includes(ext);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validFiles.length === 0) {
|
||||||
|
alert('No supported files selected. Please select image or video files.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get file paths to send to backend
|
||||||
|
const filePaths = validFiles.map(file => {
|
||||||
|
// We need the full path, but we only have the filename
|
||||||
|
// For security reasons, browsers don't provide full paths
|
||||||
|
// This will only work if the backend can handle just filenames
|
||||||
|
return URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use FileReader to get the file data for direct upload
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('model_hash', modelHash);
|
||||||
|
|
||||||
|
validFiles.forEach(file => {
|
||||||
|
formData.append('files', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call API to import files
|
||||||
|
const response = await fetch('/api/import-example-images', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to import example files');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get updated local files
|
||||||
|
const updatedFilesResponse = await fetch(`/api/example-image-files?model_hash=${modelHash}`);
|
||||||
|
const updatedFilesResult = await updatedFilesResponse.json();
|
||||||
|
|
||||||
|
if (!updatedFilesResult.success) {
|
||||||
|
throw new Error(updatedFilesResult.error || 'Failed to get updated file list');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render the showcase content
|
||||||
|
const showcaseTab = document.getElementById('showcase-tab');
|
||||||
|
if (showcaseTab) {
|
||||||
|
// Get the updated images from the result
|
||||||
|
const updatedImages = result.updated_images || [];
|
||||||
|
showcaseTab.innerHTML = renderShowcaseContent(updatedImages, updatedFilesResult.files);
|
||||||
|
|
||||||
|
// Re-initialize showcase functionality
|
||||||
|
const carousel = showcaseTab.querySelector('.carousel');
|
||||||
|
if (carousel) {
|
||||||
|
if (!carousel.classList.contains('collapsed')) {
|
||||||
|
initLazyLoading(carousel);
|
||||||
|
initNsfwBlurHandlers(carousel);
|
||||||
|
initMetadataPanelHandlers(carousel);
|
||||||
|
}
|
||||||
|
// Initialize the import UI for the new content
|
||||||
|
initExampleImport(modelHash, showcaseTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update VirtualScroller if available
|
||||||
|
if (state.virtualScroller && result.model_file_path) {
|
||||||
|
// Create an update object with only the necessary properties
|
||||||
|
const updateData = {
|
||||||
|
civitai: {
|
||||||
|
images: updatedImages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the item in the virtual scroller
|
||||||
|
state.virtualScroller.updateSingleItem(result.model_file_path, updateData);
|
||||||
|
console.log('Updated VirtualScroller item with new example images');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing examples:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate metadata panel HTML
|
* Generate metadata panel HTML
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
import { showToast, copyToClipboard, getExampleImageFiles } from '../../utils/uiHelpers.js';
|
import { showToast, copyToClipboard, getExampleImageFiles } from '../../utils/uiHelpers.js';
|
||||||
import { modalManager } from '../../managers/ModalManager.js';
|
import { modalManager } from '../../managers/ModalManager.js';
|
||||||
import { renderShowcaseContent, toggleShowcase, setupShowcaseScroll, scrollToTop } from './ShowcaseView.js';
|
import {
|
||||||
|
renderShowcaseContent,
|
||||||
|
toggleShowcase,
|
||||||
|
setupShowcaseScroll,
|
||||||
|
scrollToTop,
|
||||||
|
initExampleImport
|
||||||
|
} from './ShowcaseView.js';
|
||||||
import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
|
import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
|
||||||
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
|
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
|
||||||
import { parsePresets, renderPresetTags } from './PresetTags.js';
|
import { parsePresets, renderPresetTags } from './PresetTags.js';
|
||||||
@@ -207,14 +213,8 @@ async function loadExampleImages(images, modelHash, filePath) {
|
|||||||
let localFiles = [];
|
let localFiles = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Choose endpoint based on centralized examples setting
|
const endpoint = '/api/example-image-files';
|
||||||
const useCentralized = state.global.settings.useCentralizedExamples !== false;
|
const params = `model_hash=${modelHash}`;
|
||||||
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 response = await fetch(`${endpoint}?${params}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -239,6 +239,9 @@ async function loadExampleImages(images, modelHash, filePath) {
|
|||||||
initMetadataPanelHandlers(carousel);
|
initMetadataPanelHandlers(carousel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the example import functionality
|
||||||
|
initExampleImport(modelHash, showcaseTab);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading example images:', error);
|
console.error('Error loading example images:', error);
|
||||||
const showcaseTab = document.getElementById('showcase-tab');
|
const showcaseTab = document.getElementById('showcase-tab');
|
||||||
|
|||||||
Reference in New Issue
Block a user