mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Refactor recipe management to enhance initialization and metadata handling. Improve error logging during cache pre-warming, streamline recipe data structure, and ensure proper handling of generation parameters. Update UI components for missing LoRAs with improved summary and toggle functionality. Add new methods for adding recipes to cache and loading recipe data from JSON files.
This commit is contained in:
@@ -48,14 +48,26 @@ class RecipeRoutes:
|
|||||||
"""Initialize cache on startup"""
|
"""Initialize cache on startup"""
|
||||||
print("Pre-warming recipe cache...", file=sys.stderr)
|
print("Pre-warming recipe cache...", file=sys.stderr)
|
||||||
try:
|
try:
|
||||||
# Diagnose lora scanner first
|
# First, ensure the lora scanner is fully initialized
|
||||||
await self.recipe_scanner._lora_scanner.diagnose_hash_index()
|
print("Initializing lora scanner...", file=sys.stderr)
|
||||||
|
lora_scanner = self.recipe_scanner._lora_scanner
|
||||||
|
|
||||||
# Force a cache refresh
|
# Get lora cache to ensure it's initialized
|
||||||
|
lora_cache = await lora_scanner.get_cached_data()
|
||||||
|
print(f"Lora scanner initialized with {len(lora_cache.raw_data)} loras", file=sys.stderr)
|
||||||
|
|
||||||
|
# Verify hash index is built
|
||||||
|
if hasattr(lora_scanner, '_hash_index'):
|
||||||
|
hash_index_size = len(lora_scanner._hash_index._hash_to_path) if hasattr(lora_scanner._hash_index, '_hash_to_path') else 0
|
||||||
|
print(f"Lora hash index contains {hash_index_size} entries", file=sys.stderr)
|
||||||
|
|
||||||
|
# Now that lora scanner is initialized, initialize recipe cache
|
||||||
|
print("Initializing recipe cache...", file=sys.stderr)
|
||||||
await self.recipe_scanner.get_cached_data(force_refresh=True)
|
await self.recipe_scanner.get_cached_data(force_refresh=True)
|
||||||
print("Recipe cache pre-warming complete", file=sys.stderr)
|
print("Recipe cache pre-warming complete", file=sys.stderr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error pre-warming recipe cache: {e}", file=sys.stderr)
|
print(f"Error pre-warming recipe cache: {e}", file=sys.stderr)
|
||||||
|
logger.error(f"Error pre-warming recipe cache: {e}", exc_info=True)
|
||||||
|
|
||||||
async def get_recipes(self, request: web.Request) -> web.Response:
|
async def get_recipes(self, request: web.Request) -> web.Response:
|
||||||
"""API endpoint for getting paginated recipes"""
|
"""API endpoint for getting paginated recipes"""
|
||||||
@@ -221,6 +233,7 @@ class RecipeRoutes:
|
|||||||
# Check if this LoRA exists locally by SHA256 hash
|
# Check if this LoRA exists locally by SHA256 hash
|
||||||
exists_locally = False
|
exists_locally = False
|
||||||
local_path = None
|
local_path = None
|
||||||
|
sha256 = ''
|
||||||
|
|
||||||
if civitai_info and 'files' in civitai_info:
|
if civitai_info and 'files' in civitai_info:
|
||||||
# Find the model file (type="Model") in the files list
|
# Find the model file (type="Model") in the files list
|
||||||
@@ -234,7 +247,7 @@ class RecipeRoutes:
|
|||||||
if exists_locally:
|
if exists_locally:
|
||||||
local_path = self.recipe_scanner._lora_scanner.get_lora_path_by_hash(sha256)
|
local_path = self.recipe_scanner._lora_scanner.get_lora_path_by_hash(sha256)
|
||||||
|
|
||||||
# Create LoRA entry
|
# Create LoRA entry for frontend display
|
||||||
lora_entry = {
|
lora_entry = {
|
||||||
'id': model_version_id,
|
'id': model_version_id,
|
||||||
'name': resource.get('modelName', ''),
|
'name': resource.get('modelName', ''),
|
||||||
@@ -243,6 +256,8 @@ class RecipeRoutes:
|
|||||||
'weight': resource.get('weight', 1.0),
|
'weight': resource.get('weight', 1.0),
|
||||||
'existsLocally': exists_locally,
|
'existsLocally': exists_locally,
|
||||||
'localPath': local_path,
|
'localPath': local_path,
|
||||||
|
'file_name': os.path.splitext(os.path.basename(local_path))[0] if local_path else '',
|
||||||
|
'hash': sha256,
|
||||||
'thumbnailUrl': '',
|
'thumbnailUrl': '',
|
||||||
'baseModel': '',
|
'baseModel': '',
|
||||||
'size': 0,
|
'size': 0,
|
||||||
@@ -267,9 +282,24 @@ class RecipeRoutes:
|
|||||||
|
|
||||||
loras.append(lora_entry)
|
loras.append(lora_entry)
|
||||||
|
|
||||||
|
# Extract generation parameters for recipe metadata
|
||||||
|
gen_params = {
|
||||||
|
'prompt': metadata.get('prompt', ''),
|
||||||
|
'negative_prompt': metadata.get('negative_prompt', ''),
|
||||||
|
'checkpoint': checkpoint,
|
||||||
|
'steps': metadata.get('steps', ''),
|
||||||
|
'sampler': metadata.get('sampler', ''),
|
||||||
|
'cfg_scale': metadata.get('cfg_scale', ''),
|
||||||
|
'seed': metadata.get('seed', ''),
|
||||||
|
'size': metadata.get('size', ''),
|
||||||
|
'clip_skip': metadata.get('clip_skip', '')
|
||||||
|
}
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'base_model': base_model,
|
'base_model': base_model,
|
||||||
'loras': loras
|
'loras': loras,
|
||||||
|
'gen_params': gen_params,
|
||||||
|
'raw_metadata': metadata # Include the raw metadata for saving
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -350,6 +380,39 @@ class RecipeRoutes:
|
|||||||
|
|
||||||
# Create the recipe JSON
|
# Create the recipe JSON
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Format loras data according to the recipe.json format
|
||||||
|
loras_data = []
|
||||||
|
for lora in metadata.get("loras", []):
|
||||||
|
# Convert frontend lora format to recipe format
|
||||||
|
lora_entry = {
|
||||||
|
"file_name": lora.get("file_name", "") or os.path.splitext(os.path.basename(lora.get("localPath", "")))[0],
|
||||||
|
"hash": lora.get("hash", "").lower() if lora.get("hash") else "",
|
||||||
|
"strength": float(lora.get("weight", 1.0)),
|
||||||
|
"modelVersionId": lora.get("id", ""),
|
||||||
|
"modelName": lora.get("name", ""),
|
||||||
|
"modelVersionName": lora.get("version", "")
|
||||||
|
}
|
||||||
|
loras_data.append(lora_entry)
|
||||||
|
|
||||||
|
# Format gen_params according to the recipe.json format
|
||||||
|
gen_params = metadata.get("gen_params", {})
|
||||||
|
if not gen_params and "raw_metadata" in metadata:
|
||||||
|
# Extract from raw metadata if available
|
||||||
|
raw_metadata = metadata.get("raw_metadata", {})
|
||||||
|
gen_params = {
|
||||||
|
"prompt": raw_metadata.get("prompt", ""),
|
||||||
|
"negative_prompt": raw_metadata.get("negative_prompt", ""),
|
||||||
|
"checkpoint": raw_metadata.get("checkpoint", {}),
|
||||||
|
"steps": raw_metadata.get("steps", ""),
|
||||||
|
"sampler": raw_metadata.get("sampler", ""),
|
||||||
|
"cfg_scale": raw_metadata.get("cfg_scale", ""),
|
||||||
|
"seed": raw_metadata.get("seed", ""),
|
||||||
|
"size": raw_metadata.get("size", ""),
|
||||||
|
"clip_skip": raw_metadata.get("clip_skip", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the recipe data structure
|
||||||
recipe_data = {
|
recipe_data = {
|
||||||
"id": recipe_id,
|
"id": recipe_id,
|
||||||
"file_path": image_path,
|
"file_path": image_path,
|
||||||
@@ -357,8 +420,8 @@ class RecipeRoutes:
|
|||||||
"modified": current_time,
|
"modified": current_time,
|
||||||
"created_date": current_time,
|
"created_date": current_time,
|
||||||
"base_model": metadata.get("base_model", ""),
|
"base_model": metadata.get("base_model", ""),
|
||||||
"loras": metadata.get("loras", []),
|
"loras": loras_data,
|
||||||
"gen_params": metadata.get("gen_params", {})
|
"gen_params": gen_params
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add tags if provided
|
# Add tags if provided
|
||||||
@@ -370,8 +433,11 @@ class RecipeRoutes:
|
|||||||
json_path = os.path.join(recipes_dir, json_filename)
|
json_path = os.path.join(recipes_dir, json_filename)
|
||||||
with open(json_path, 'w', encoding='utf-8') as f:
|
with open(json_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(recipe_data, f, indent=4, ensure_ascii=False)
|
json.dump(recipe_data, f, indent=4, ensure_ascii=False)
|
||||||
# Force refresh the recipe cache
|
|
||||||
await self.recipe_scanner.get_cached_data(force_refresh=True)
|
# Add the new recipe directly to the cache instead of forcing a refresh
|
||||||
|
cache = await self.recipe_scanner.get_cached_data()
|
||||||
|
await cache.add_recipe(recipe_data)
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'recipe_id': recipe_id,
|
'recipe_id': recipe_id,
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ class RecipeCache:
|
|||||||
reverse=True
|
reverse=True
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update_recipe_metadata(self, file_path: str, metadata: Dict) -> bool:
|
async def update_recipe_metadata(self, recipe_id: str, metadata: Dict) -> bool:
|
||||||
"""Update metadata for a specific recipe in all cached data
|
"""Update metadata for a specific recipe in all cached data
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: The file path of the recipe to update
|
recipe_id: The ID of the recipe to update
|
||||||
metadata: The new metadata
|
metadata: The new metadata
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -40,7 +40,7 @@ class RecipeCache:
|
|||||||
async with self._lock:
|
async with self._lock:
|
||||||
# Update in raw_data
|
# Update in raw_data
|
||||||
for item in self.raw_data:
|
for item in self.raw_data:
|
||||||
if item['file_path'] == file_path:
|
if item.get('id') == recipe_id:
|
||||||
item.update(metadata)
|
item.update(metadata)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -48,4 +48,14 @@ class RecipeCache:
|
|||||||
|
|
||||||
# Resort to reflect changes
|
# Resort to reflect changes
|
||||||
await self.resort()
|
await self.resort()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def add_recipe(self, recipe_data: Dict) -> None:
|
||||||
|
"""Add a new recipe to the cache
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recipe_data: The recipe data to add
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
self.raw_data.append(recipe_data)
|
||||||
|
await self.resort()
|
||||||
@@ -5,49 +5,13 @@ import json
|
|||||||
import re
|
import re
|
||||||
from typing import List, Dict, Optional, Any
|
from typing import List, Dict, Optional, Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..utils.exif_utils import ExifUtils
|
|
||||||
from ..config import config
|
from ..config import config
|
||||||
from .recipe_cache import RecipeCache
|
from .recipe_cache import RecipeCache
|
||||||
from .lora_scanner import LoraScanner
|
from .lora_scanner import LoraScanner
|
||||||
from .civitai_client import CivitaiClient
|
from .civitai_client import CivitaiClient
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
print("Recipe Scanner module loaded", file=sys.stderr)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def setup_logger():
|
|
||||||
"""Configure logger for recipe scanner"""
|
|
||||||
# First, print directly to stderr
|
|
||||||
print("Setting up recipe scanner logger", file=sys.stderr)
|
|
||||||
|
|
||||||
# Create a stderr handler
|
|
||||||
handler = logging.StreamHandler(sys.stderr)
|
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
|
|
||||||
# Configure recipe logger
|
|
||||||
recipe_logger = logging.getLogger(__name__)
|
|
||||||
recipe_logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# Remove existing handlers if any
|
|
||||||
for h in recipe_logger.handlers:
|
|
||||||
recipe_logger.removeHandler(h)
|
|
||||||
|
|
||||||
recipe_logger.addHandler(handler)
|
|
||||||
recipe_logger.propagate = False
|
|
||||||
|
|
||||||
# Also ensure the root logger has a handler
|
|
||||||
root_logger = logging.getLogger()
|
|
||||||
root_logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# Check if the root logger already has handlers
|
|
||||||
if not root_logger.handlers:
|
|
||||||
root_logger.addHandler(handler)
|
|
||||||
|
|
||||||
print(f"Logger setup complete: {__name__}", file=sys.stderr)
|
|
||||||
return recipe_logger
|
|
||||||
|
|
||||||
# Use our configured logger
|
|
||||||
logger = setup_logger()
|
|
||||||
|
|
||||||
class RecipeScanner:
|
class RecipeScanner:
|
||||||
"""Service for scanning and managing recipe images"""
|
"""Service for scanning and managing recipe images"""
|
||||||
@@ -132,13 +96,7 @@ class RecipeScanner:
|
|||||||
if self._lora_scanner:
|
if self._lora_scanner:
|
||||||
logger.info("Recipe Manager: Waiting for lora scanner initialization to complete")
|
logger.info("Recipe Manager: Waiting for lora scanner initialization to complete")
|
||||||
|
|
||||||
# Force a fresh initialization of the lora scanner to ensure it's complete
|
# Get the lora cache to ensure it's initialized
|
||||||
lora_cache = await self._lora_scanner.get_cached_data(force_refresh=True)
|
|
||||||
|
|
||||||
# Add a delay to ensure any background tasks complete
|
|
||||||
await asyncio.sleep(2)
|
|
||||||
|
|
||||||
# Get the cache again to ensure we have the latest data
|
|
||||||
lora_cache = await self._lora_scanner.get_cached_data()
|
lora_cache = await self._lora_scanner.get_cached_data()
|
||||||
logger.info(f"Recipe Manager: Lora scanner initialized with {len(lora_cache.raw_data)} loras")
|
logger.info(f"Recipe Manager: Lora scanner initialized with {len(lora_cache.raw_data)} loras")
|
||||||
|
|
||||||
@@ -146,18 +104,6 @@ class RecipeScanner:
|
|||||||
if hasattr(self._lora_scanner, '_hash_index'):
|
if hasattr(self._lora_scanner, '_hash_index'):
|
||||||
hash_index_size = len(self._lora_scanner._hash_index._hash_to_path) if hasattr(self._lora_scanner._hash_index, '_hash_to_path') else 0
|
hash_index_size = len(self._lora_scanner._hash_index._hash_to_path) if hasattr(self._lora_scanner._hash_index, '_hash_to_path') else 0
|
||||||
logger.info(f"Recipe Manager: Lora hash index contains {hash_index_size} entries")
|
logger.info(f"Recipe Manager: Lora hash index contains {hash_index_size} entries")
|
||||||
|
|
||||||
# If hash index is empty but we have loras, consider this an error condition
|
|
||||||
if hash_index_size == 0 and len(lora_cache.raw_data) > 0:
|
|
||||||
logger.error("Recipe Manager: Lora hash index is empty despite having loras in cache")
|
|
||||||
await self._lora_scanner.diagnose_hash_index()
|
|
||||||
|
|
||||||
# Wait another moment for hash index to potentially initialize
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
# Try to check again
|
|
||||||
hash_index_size = len(self._lora_scanner._hash_index._hash_to_path) if hasattr(self._lora_scanner._hash_index, '_hash_to_path') else 0
|
|
||||||
logger.info(f"Recipe Manager: Lora hash index now contains {hash_index_size} entries")
|
|
||||||
else:
|
else:
|
||||||
logger.warning("Recipe Manager: No lora hash index available")
|
logger.warning("Recipe Manager: No lora hash index available")
|
||||||
else:
|
else:
|
||||||
@@ -187,7 +133,7 @@ class RecipeScanner:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def scan_all_recipes(self) -> List[Dict]:
|
async def scan_all_recipes(self) -> List[Dict]:
|
||||||
"""Scan all recipe images and return metadata"""
|
"""Scan all recipe JSON files and return metadata"""
|
||||||
recipes = []
|
recipes = []
|
||||||
recipes_dir = self.recipes_dir
|
recipes_dir = self.recipes_dir
|
||||||
|
|
||||||
@@ -195,20 +141,20 @@ class RecipeScanner:
|
|||||||
logger.warning(f"Recipes directory not found: {recipes_dir}")
|
logger.warning(f"Recipes directory not found: {recipes_dir}")
|
||||||
return recipes
|
return recipes
|
||||||
|
|
||||||
# Get all jpg/jpeg files in the recipes directory
|
# Get all recipe JSON files in the recipes directory
|
||||||
image_files = []
|
recipe_files = []
|
||||||
logger.info(f"Scanning for recipe images in {recipes_dir}")
|
logger.info(f"Scanning for recipe JSON files in {recipes_dir}")
|
||||||
for root, _, files in os.walk(recipes_dir):
|
for root, _, files in os.walk(recipes_dir):
|
||||||
image_count = sum(1 for f in files if f.lower().endswith(('.jpg', '.jpeg')))
|
recipe_count = sum(1 for f in files if f.lower().endswith('.recipe.json'))
|
||||||
if image_count > 0:
|
if recipe_count > 0:
|
||||||
logger.info(f"Found {image_count} potential recipe images in {root}")
|
logger.info(f"Found {recipe_count} recipe files in {root}")
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.lower().endswith(('.jpg', '.jpeg')):
|
if file.lower().endswith('.recipe.json'):
|
||||||
image_files.append(os.path.join(root, file))
|
recipe_files.append(os.path.join(root, file))
|
||||||
|
|
||||||
# Process each image
|
# Process each recipe file
|
||||||
for image_path in image_files:
|
for recipe_path in recipe_files:
|
||||||
recipe_data = await self._process_recipe_image(image_path)
|
recipe_data = await self._load_recipe_file(recipe_path)
|
||||||
if recipe_data:
|
if recipe_data:
|
||||||
recipes.append(recipe_data)
|
recipes.append(recipe_data)
|
||||||
logger.info(f"Processed recipe: {recipe_data.get('title')}")
|
logger.info(f"Processed recipe: {recipe_data.get('title')}")
|
||||||
@@ -217,87 +163,54 @@ class RecipeScanner:
|
|||||||
|
|
||||||
return recipes
|
return recipes
|
||||||
|
|
||||||
async def _process_recipe_image(self, image_path: str) -> Optional[Dict]:
|
async def _load_recipe_file(self, recipe_path: str) -> Optional[Dict]:
|
||||||
"""Process a single recipe image and return metadata"""
|
"""Load recipe data from a JSON file"""
|
||||||
try:
|
try:
|
||||||
print(f"Processing recipe image: {image_path}", file=sys.stderr)
|
logger.info(f"Loading recipe file: {recipe_path}")
|
||||||
logger.info(f"Processing recipe image: {image_path}")
|
|
||||||
|
|
||||||
# Extract EXIF UserComment
|
with open(recipe_path, 'r', encoding='utf-8') as f:
|
||||||
user_comment = ExifUtils.extract_user_comment(image_path)
|
recipe_data = json.load(f)
|
||||||
if not user_comment:
|
|
||||||
print(f"No EXIF UserComment found in {image_path}", file=sys.stderr)
|
|
||||||
logger.warning(f"No EXIF UserComment found in {image_path}")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
print(f"Found UserComment: {user_comment[:50]}...", file=sys.stderr)
|
|
||||||
|
|
||||||
# Parse generation parameters from UserComment
|
# Validate recipe data
|
||||||
gen_params = ExifUtils.parse_recipe_metadata(user_comment)
|
if not recipe_data or not isinstance(recipe_data, dict):
|
||||||
if not gen_params:
|
logger.warning(f"Invalid recipe data in {recipe_path}")
|
||||||
print(f"Failed to parse recipe metadata from {image_path}", file=sys.stderr)
|
|
||||||
logger.warning(f"Failed to parse recipe metadata from {image_path}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get file info
|
# Ensure required fields exist
|
||||||
stat = os.stat(image_path)
|
required_fields = ['id', 'file_path', 'title']
|
||||||
file_name = os.path.basename(image_path)
|
for field in required_fields:
|
||||||
title = os.path.splitext(file_name)[0]
|
if field not in recipe_data:
|
||||||
|
logger.warning(f"Missing required field '{field}' in {recipe_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
# Check for existing recipe metadata
|
# Ensure the image file exists
|
||||||
recipe_data = self._extract_recipe_metadata(user_comment)
|
image_path = recipe_data.get('file_path')
|
||||||
if not recipe_data:
|
if not os.path.exists(image_path):
|
||||||
# Create new recipe data
|
logger.warning(f"Recipe image not found: {image_path}")
|
||||||
recipe_data = {
|
# Try to find the image in the same directory as the recipe
|
||||||
'id': file_name,
|
recipe_dir = os.path.dirname(recipe_path)
|
||||||
'file_path': image_path,
|
image_filename = os.path.basename(image_path)
|
||||||
'title': title,
|
alternative_path = os.path.join(recipe_dir, image_filename)
|
||||||
'modified': stat.st_mtime,
|
if os.path.exists(alternative_path):
|
||||||
'created_date': stat.st_ctime,
|
logger.info(f"Found alternative image path: {alternative_path}")
|
||||||
'file_size': stat.st_size,
|
recipe_data['file_path'] = alternative_path
|
||||||
'loras': [],
|
else:
|
||||||
'gen_params': {}
|
logger.warning(f"Could not find alternative image path for {image_path}")
|
||||||
}
|
|
||||||
|
|
||||||
# Copy loras from gen_params to recipe_data with proper structure
|
|
||||||
for lora in gen_params.get('loras', []):
|
|
||||||
recipe_lora = {
|
|
||||||
'file_name': '',
|
|
||||||
'hash': lora.get('hash', '').lower() if lora.get('hash') else '',
|
|
||||||
'strength': lora.get('weight', 1.0),
|
|
||||||
'modelVersionId': lora.get('modelVersionId', ''),
|
|
||||||
'modelName': lora.get('modelName', ''),
|
|
||||||
'modelVersionName': lora.get('modelVersionName', '')
|
|
||||||
}
|
|
||||||
recipe_data['loras'].append(recipe_lora)
|
|
||||||
|
|
||||||
# Add generation parameters to recipe_data.gen_params instead of top level
|
# Ensure loras array exists
|
||||||
recipe_data['gen_params'] = {
|
if 'loras' not in recipe_data:
|
||||||
'prompt': gen_params.get('prompt', ''),
|
recipe_data['loras'] = []
|
||||||
'negative_prompt': gen_params.get('negative_prompt', ''),
|
|
||||||
'checkpoint': gen_params.get('checkpoint', None),
|
|
||||||
'steps': gen_params.get('steps', ''),
|
|
||||||
'sampler': gen_params.get('sampler', ''),
|
|
||||||
'cfg_scale': gen_params.get('cfg_scale', ''),
|
|
||||||
'seed': gen_params.get('seed', ''),
|
|
||||||
'size': gen_params.get('size', ''),
|
|
||||||
'clip_skip': gen_params.get('clip_skip', '')
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update recipe metadata with missing information
|
# Ensure gen_params exists
|
||||||
metadata_updated = await self._update_recipe_metadata(recipe_data, user_comment)
|
if 'gen_params' not in recipe_data:
|
||||||
recipe_data['_metadata_updated'] = metadata_updated
|
recipe_data['gen_params'] = {}
|
||||||
|
|
||||||
# If metadata was updated, save back to image
|
# Update lora information with local paths and availability
|
||||||
if metadata_updated:
|
await self._update_lora_information(recipe_data)
|
||||||
print(f"Updating metadata for {image_path}", file=sys.stderr)
|
|
||||||
logger.info(f"Updating metadata for {image_path}")
|
|
||||||
self._save_updated_metadata(image_path, user_comment, recipe_data)
|
|
||||||
|
|
||||||
return recipe_data
|
return recipe_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing recipe image {image_path}: {e}", file=sys.stderr)
|
logger.error(f"Error loading recipe file {recipe_path}: {e}")
|
||||||
logger.error(f"Error processing recipe image {image_path}: {e}")
|
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc(file=sys.stderr)
|
traceback.print_exc(file=sys.stderr)
|
||||||
return None
|
return None
|
||||||
@@ -438,97 +351,7 @@ class RecipeScanner:
|
|||||||
logger.error(f"Error getting hash from Civitai: {e}")
|
logger.error(f"Error getting hash from Civitai: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _save_updated_metadata(self, image_path: str, original_comment: str, recipe_data: Dict) -> None:
|
async def _update_recipe_metadata(self, recipe_data: Dict) -> bool:
|
||||||
"""Save updated metadata back to image file"""
|
|
||||||
try:
|
|
||||||
# Check if we already have a recipe metadata section
|
|
||||||
recipe_metadata_exists = "recipe metadata:" in original_comment.lower()
|
|
||||||
|
|
||||||
# Prepare recipe metadata
|
|
||||||
recipe_metadata = {
|
|
||||||
'id': recipe_data.get('id', ''),
|
|
||||||
'file_path': recipe_data.get('file_path', ''),
|
|
||||||
'title': recipe_data.get('title', ''),
|
|
||||||
'modified': recipe_data.get('modified', 0),
|
|
||||||
'created_date': recipe_data.get('created_date', 0),
|
|
||||||
'base_model': recipe_data.get('base_model', ''),
|
|
||||||
'loras': [],
|
|
||||||
'gen_params': recipe_data.get('gen_params', {})
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add lora data with only necessary fields (removing weight, adding modelVersionName)
|
|
||||||
for lora in recipe_data.get('loras', []):
|
|
||||||
lora_entry = {
|
|
||||||
'file_name': lora.get('file_name', ''),
|
|
||||||
'hash': lora.get('hash', '').lower() if lora.get('hash') else '',
|
|
||||||
'strength': lora.get('strength', 1.0),
|
|
||||||
'modelVersionId': lora.get('modelVersionId', ''),
|
|
||||||
'modelName': lora.get('modelName', ''),
|
|
||||||
'modelVersionName': lora.get('modelVersionName', '')
|
|
||||||
}
|
|
||||||
recipe_metadata['loras'].append(lora_entry)
|
|
||||||
|
|
||||||
# Convert to JSON
|
|
||||||
recipe_metadata_json = json.dumps(recipe_metadata)
|
|
||||||
|
|
||||||
# Create or update the recipe metadata section
|
|
||||||
if recipe_metadata_exists:
|
|
||||||
# Replace existing recipe metadata
|
|
||||||
updated_comment = re.sub(
|
|
||||||
r'recipe metadata: \{.*\}',
|
|
||||||
f'recipe metadata: {recipe_metadata_json}',
|
|
||||||
original_comment,
|
|
||||||
flags=re.IGNORECASE | re.DOTALL
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Append recipe metadata to the end
|
|
||||||
updated_comment = f"{original_comment}, recipe metadata: {recipe_metadata_json}"
|
|
||||||
|
|
||||||
# Save back to image
|
|
||||||
logger.info(f"Saving updated metadata to {image_path}")
|
|
||||||
ExifUtils.update_user_comment(image_path, updated_comment)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error saving updated metadata: {e}", exc_info=True)
|
|
||||||
|
|
||||||
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'date', search: str = None):
|
|
||||||
"""Get paginated and filtered recipe data
|
|
||||||
|
|
||||||
Args:
|
|
||||||
page: Current page number (1-based)
|
|
||||||
page_size: Number of items per page
|
|
||||||
sort_by: Sort method ('name' or 'date')
|
|
||||||
search: Search term
|
|
||||||
"""
|
|
||||||
cache = await self.get_cached_data()
|
|
||||||
|
|
||||||
# Get base dataset
|
|
||||||
filtered_data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name
|
|
||||||
|
|
||||||
# Apply search filter
|
|
||||||
if search:
|
|
||||||
filtered_data = [
|
|
||||||
item for item in filtered_data
|
|
||||||
if search.lower() in str(item.get('title', '')).lower() or
|
|
||||||
search.lower() in str(item.get('prompt', '')).lower()
|
|
||||||
]
|
|
||||||
|
|
||||||
# Calculate pagination
|
|
||||||
total_items = len(filtered_data)
|
|
||||||
start_idx = (page - 1) * page_size
|
|
||||||
end_idx = min(start_idx + page_size, total_items)
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'items': filtered_data[start_idx:end_idx],
|
|
||||||
'total': total_items,
|
|
||||||
'page': page,
|
|
||||||
'page_size': page_size,
|
|
||||||
'total_pages': (total_items + page_size - 1) // page_size
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def _update_recipe_metadata(self, recipe_data: Dict, original_comment: str) -> bool:
|
|
||||||
"""Update recipe metadata with missing information
|
"""Update recipe metadata with missing information
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -587,9 +410,9 @@ class RecipeScanner:
|
|||||||
metadata_updated = True
|
metadata_updated = True
|
||||||
|
|
||||||
# Determine the base_model for the recipe based on loras
|
# Determine the base_model for the recipe based on loras
|
||||||
if recipe_data.get('loras'):
|
if recipe_data.get('loras') and not recipe_data.get('base_model'):
|
||||||
base_model = await self._determine_base_model(recipe_data.get('loras', []))
|
base_model = await self._determine_base_model(recipe_data.get('loras', []))
|
||||||
if base_model and (not recipe_data.get('base_model') or recipe_data['base_model'] != base_model):
|
if base_model:
|
||||||
recipe_data['base_model'] = base_model
|
recipe_data['base_model'] = base_model
|
||||||
metadata_updated = True
|
metadata_updated = True
|
||||||
|
|
||||||
@@ -651,22 +474,49 @@ class RecipeScanner:
|
|||||||
logger.error(f"Error getting base model for lora: {e}")
|
logger.error(f"Error getting base model for lora: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _extract_recipe_metadata(self, user_comment: str) -> Optional[Dict]:
|
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'date', search: str = None):
|
||||||
"""Extract recipe metadata section from UserComment if it exists"""
|
"""Get paginated and filtered recipe data
|
||||||
try:
|
|
||||||
# Look for recipe metadata section
|
Args:
|
||||||
recipe_match = re.search(r'recipe metadata: (\{.*\})', user_comment, re.IGNORECASE | re.DOTALL)
|
page: Current page number (1-based)
|
||||||
if not recipe_match:
|
page_size: Number of items per page
|
||||||
return None
|
sort_by: Sort method ('name' or 'date')
|
||||||
|
search: Search term
|
||||||
recipe_json = recipe_match.group(1)
|
"""
|
||||||
recipe_data = json.loads(recipe_json)
|
cache = await self.get_cached_data()
|
||||||
|
|
||||||
# Ensure loras array exists
|
# Get base dataset
|
||||||
if 'loras' not in recipe_data:
|
filtered_data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name
|
||||||
recipe_data['loras'] = []
|
|
||||||
|
# Apply search filter
|
||||||
return recipe_data
|
if search:
|
||||||
except Exception as e:
|
filtered_data = [
|
||||||
logger.error(f"Error extracting recipe metadata: {e}")
|
item for item in filtered_data
|
||||||
return None
|
if search.lower() in str(item.get('title', '')).lower() or
|
||||||
|
search.lower() in str(item.get('prompt', '')).lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Calculate pagination
|
||||||
|
total_items = len(filtered_data)
|
||||||
|
start_idx = (page - 1) * page_size
|
||||||
|
end_idx = min(start_idx + page_size, total_items)
|
||||||
|
|
||||||
|
# Get paginated items
|
||||||
|
paginated_items = filtered_data[start_idx:end_idx]
|
||||||
|
|
||||||
|
# Add inLibrary information for each lora
|
||||||
|
for item in paginated_items:
|
||||||
|
if 'loras' in item:
|
||||||
|
for lora in item['loras']:
|
||||||
|
if 'hash' in lora and lora['hash']:
|
||||||
|
lora['inLibrary'] = self._lora_scanner.has_lora_hash(lora['hash'].lower())
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'items': paginated_items,
|
||||||
|
'total': total_items,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'total_pages': (total_items + page_size - 1) // page_size
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "3",
|
"id": "0448c06d-de1b-46ab-975c-c5aa60d90dbc",
|
||||||
"file_path": "D:/Workspace/ComfyUI/models/loras/recipes/3.jpg",
|
"file_path": "D:/Workspace/ComfyUI/models/loras/recipes/0448c06d-de1b-46ab-975c-c5aa60d90dbc.jpg",
|
||||||
"title": "3",
|
"title": "a mysterious, steampunk-inspired character standing in a dramatic pose",
|
||||||
"modified": 1741837612.3931093,
|
"modified": 1741837612.3931093,
|
||||||
"created_date": 1741492786.5581934,
|
"created_date": 1741492786.5581934,
|
||||||
"base_model": "Flux.1 D",
|
"base_model": "Flux.1 D",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"modelVersionName": "ink-dynamic"
|
"modelVersionName": "ink-dynamic"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file_name": "",
|
"file_name": "ck-painterly-fantasy-000017",
|
||||||
"hash": "48c67064e2936aec342580a2a729d91d75eb818e45ecf993b9650cc66c94c420",
|
"hash": "48c67064e2936aec342580a2a729d91d75eb818e45ecf993b9650cc66c94c420",
|
||||||
"strength": 0.2,
|
"strength": 0.2,
|
||||||
"modelVersionId": 1189379,
|
"modelVersionId": 1189379,
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"modelVersionName": "v1.0"
|
"modelVersionName": "v1.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file_name": "",
|
"file_name": "Mezzotint_Artstyle_for_Flux_-_by_Ethanar",
|
||||||
"hash": "e6961502769123bf23a66c5c5298d76264fd6b9610f018319a0ccb091bfc308e",
|
"hash": "e6961502769123bf23a66c5c5298d76264fd6b9610f018319a0ccb091bfc308e",
|
||||||
"strength": 0.2,
|
"strength": 0.2,
|
||||||
"modelVersionId": 757030,
|
"modelVersionId": 757030,
|
||||||
|
|||||||
@@ -395,26 +395,77 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Missing LoRAs summary section */
|
/* Improved Missing LoRAs summary section */
|
||||||
.missing-loras-summary {
|
.missing-loras-summary {
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
border: 1px solid var(--lora-error);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.missing-loras-summary h3 {
|
.summary-header {
|
||||||
margin-top: 0;
|
display: flex;
|
||||||
margin-bottom: var(--space-2);
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-header h3 {
|
||||||
|
margin: 0;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-size-info {
|
.lora-count-badge {
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
font-weight: normal;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-size-badge {
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: normal;
|
||||||
|
background: var(--lora-surface);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
margin-left: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-list-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-list-btn:hover {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-loras-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: max-height 0.3s ease, margin-top 0.3s ease, padding-top 0.3s ease;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-loras-list.collapsed {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-loras-list:not(.collapsed) {
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
padding-top: var(--space-1);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.missing-lora-item {
|
.missing-lora-item {
|
||||||
@@ -429,13 +480,45 @@
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.missing-lora-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.missing-lora-name {
|
.missing-lora-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
flex: 1;
|
}
|
||||||
|
|
||||||
|
.lora-base-model {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--lora-accent);
|
||||||
|
background: oklch(var(--lora-accent) / 0.1);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.missing-lora-size {
|
.missing-lora-size {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recipe name input select-all behavior */
|
||||||
|
#recipeName:focus {
|
||||||
|
outline: 2px solid var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent layout shift with scrollbar */
|
||||||
|
.modal-content {
|
||||||
|
overflow-y: scroll; /* Always show scrollbar */
|
||||||
|
scrollbar-gutter: stable; /* Reserve space for scrollbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For browsers that don't support scrollbar-gutter */
|
||||||
|
@supports not (scrollbar-gutter: stable) {
|
||||||
|
.modal-content {
|
||||||
|
padding-right: calc(var(--space-2) + var(--scrollbar-width)); /* Add extra padding for scrollbar */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +186,11 @@ export class ImportManager {
|
|||||||
throw new Error('No LoRA information found in this image');
|
throw new Error('No LoRA information found in this image');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store generation parameters if available
|
||||||
|
if (this.recipeData.gen_params) {
|
||||||
|
console.log('Generation parameters found:', this.recipeData.gen_params);
|
||||||
|
}
|
||||||
|
|
||||||
// Find missing LoRAs
|
// Find missing LoRAs
|
||||||
this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally);
|
this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally);
|
||||||
|
|
||||||
@@ -202,9 +207,24 @@ export class ImportManager {
|
|||||||
showRecipeDetailsStep() {
|
showRecipeDetailsStep() {
|
||||||
this.showStep('detailsStep');
|
this.showStep('detailsStep');
|
||||||
|
|
||||||
// Set default recipe name from image filename
|
// Set default recipe name from prompt or image filename
|
||||||
const recipeName = document.getElementById('recipeName');
|
const recipeName = document.getElementById('recipeName');
|
||||||
if (this.recipeImage && !recipeName.value) {
|
if (this.recipeData && this.recipeData.gen_params && this.recipeData.gen_params.prompt) {
|
||||||
|
// Use the first 15 words from the prompt as the default recipe name
|
||||||
|
const promptWords = this.recipeData.gen_params.prompt.split(' ');
|
||||||
|
const truncatedPrompt = promptWords.slice(0, 15).join(' ');
|
||||||
|
recipeName.value = truncatedPrompt;
|
||||||
|
this.recipeName = truncatedPrompt;
|
||||||
|
|
||||||
|
// Set up click handler to select all text for easy editing
|
||||||
|
if (!recipeName.hasSelectAllHandler) {
|
||||||
|
recipeName.addEventListener('click', function() {
|
||||||
|
this.select();
|
||||||
|
});
|
||||||
|
recipeName.hasSelectAllHandler = true;
|
||||||
|
}
|
||||||
|
} else if (this.recipeImage && !recipeName.value) {
|
||||||
|
// Fallback to image filename if no prompt is available
|
||||||
const fileName = this.recipeImage.name.split('.')[0];
|
const fileName = this.recipeImage.name.split('.')[0];
|
||||||
recipeName.value = fileName;
|
recipeName.value = fileName;
|
||||||
this.recipeName = fileName;
|
this.recipeName = fileName;
|
||||||
@@ -386,17 +406,40 @@ export class ImportManager {
|
|||||||
totalSizeDisplay.textContent = this.formatFileSize(totalSize);
|
totalSizeDisplay.textContent = this.formatFileSize(totalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update header to include count of missing LoRAs
|
||||||
|
const missingLorasHeader = document.querySelector('.summary-header h3');
|
||||||
|
if (missingLorasHeader) {
|
||||||
|
missingLorasHeader.innerHTML = `Missing LoRAs <span class="lora-count-badge">(${this.missingLoras.length})</span> <span id="totalDownloadSize" class="total-size-badge">${this.formatFileSize(totalSize)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate missing LoRAs list
|
// Generate missing LoRAs list
|
||||||
missingLorasList.innerHTML = this.missingLoras.map(lora => {
|
missingLorasList.innerHTML = this.missingLoras.map(lora => {
|
||||||
const sizeDisplay = lora.size ? this.formatFileSize(lora.size) : 'Unknown size';
|
const sizeDisplay = lora.size ? this.formatFileSize(lora.size) : 'Unknown size';
|
||||||
|
const baseModel = lora.baseModel ? `<span class="lora-base-model">${lora.baseModel}</span>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="missing-lora-item">
|
<div class="missing-lora-item">
|
||||||
<div class="missing-lora-name">${lora.name}</div>
|
<div class="missing-lora-info">
|
||||||
|
<div class="missing-lora-name">${lora.name}</div>
|
||||||
|
${baseModel}
|
||||||
|
</div>
|
||||||
<div class="missing-lora-size">${sizeDisplay}</div>
|
<div class="missing-lora-size">${sizeDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
// Set up toggle for missing LoRAs list
|
||||||
|
const toggleBtn = document.getElementById('toggleMissingLorasList');
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.addEventListener('click', () => {
|
||||||
|
missingLorasList.classList.toggle('collapsed');
|
||||||
|
const icon = toggleBtn.querySelector('i');
|
||||||
|
if (icon) {
|
||||||
|
icon.classList.toggle('fa-chevron-down');
|
||||||
|
icon.classList.toggle('fa-chevron-up');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch LoRA roots
|
// Fetch LoRA roots
|
||||||
@@ -470,7 +513,16 @@ export class ImportManager {
|
|||||||
formData.append('image', this.recipeImage);
|
formData.append('image', this.recipeImage);
|
||||||
formData.append('name', this.recipeName);
|
formData.append('name', this.recipeName);
|
||||||
formData.append('tags', JSON.stringify(this.recipeTags));
|
formData.append('tags', JSON.stringify(this.recipeTags));
|
||||||
formData.append('metadata', JSON.stringify(this.recipeData));
|
|
||||||
|
// Prepare complete metadata including generation parameters
|
||||||
|
const completeMetadata = {
|
||||||
|
base_model: this.recipeData.base_model || "",
|
||||||
|
loras: this.recipeData.loras || [],
|
||||||
|
gen_params: this.recipeData.gen_params || {},
|
||||||
|
raw_metadata: this.recipeData.raw_metadata || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
formData.append('metadata', JSON.stringify(completeMetadata));
|
||||||
|
|
||||||
// Send save request
|
// Send save request
|
||||||
const response = await fetch('/api/recipes/save', {
|
const response = await fetch('/api/recipes/save', {
|
||||||
@@ -481,8 +533,7 @@ export class ImportManager {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Handle successful save
|
// Handle successful save
|
||||||
// Show success message for recipe save
|
|
||||||
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
|
|
||||||
|
|
||||||
// Check if we need to download LoRAs
|
// Check if we need to download LoRAs
|
||||||
if (this.missingLoras.length > 0) {
|
if (this.missingLoras.length > 0) {
|
||||||
@@ -602,6 +653,9 @@ export class ImportManager {
|
|||||||
showToast(`Downloaded ${completedDownloads} of ${this.missingLoras.length} LoRAs`, 'warning');
|
showToast(`Downloaded ${completedDownloads} of ${this.missingLoras.length} LoRAs`, 'warning');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show success message for recipe save
|
||||||
|
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
|
||||||
|
|
||||||
// Close modal and reload recipes
|
// Close modal and reload recipes
|
||||||
modalManager.closeModal('importModal');
|
modalManager.closeModal('importModal');
|
||||||
|
|||||||
@@ -68,13 +68,15 @@
|
|||||||
<!-- Step 3: Download Location (if needed) -->
|
<!-- Step 3: Download Location (if needed) -->
|
||||||
<div class="import-step" id="locationStep" style="display: none;">
|
<div class="import-step" id="locationStep" style="display: none;">
|
||||||
<div class="location-selection">
|
<div class="location-selection">
|
||||||
<!-- Add missing LoRAs summary section -->
|
<!-- Improved missing LoRAs summary section -->
|
||||||
<div class="missing-loras-summary">
|
<div class="missing-loras-summary">
|
||||||
<h3>Missing LoRAs to Download</h3>
|
<div class="summary-header">
|
||||||
<div class="total-size-info">
|
<h3>Missing LoRAs <span class="lora-count-badge">(0)</span> <span id="totalDownloadSize" class="total-size-badge">Calculating...</span></h3>
|
||||||
Total download size: <span id="totalDownloadSize">Calculating...</span>
|
<button id="toggleMissingLorasList" class="toggle-list-btn">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="missingLorasList" class="missing-loras-list">
|
<div id="missingLorasList" class="missing-loras-list collapsed">
|
||||||
<!-- Missing LoRAs will be populated here -->
|
<!-- Missing LoRAs will be populated here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user