mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
checkpoint
This commit is contained in:
@@ -37,7 +37,6 @@ class RecipeRoutes:
|
|||||||
app.router.add_get('/api/recipes', routes.get_recipes)
|
app.router.add_get('/api/recipes', routes.get_recipes)
|
||||||
app.router.add_get('/api/recipe/{recipe_id}', routes.get_recipe_detail)
|
app.router.add_get('/api/recipe/{recipe_id}', routes.get_recipe_detail)
|
||||||
app.router.add_post('/api/recipes/analyze-image', routes.analyze_recipe_image)
|
app.router.add_post('/api/recipes/analyze-image', routes.analyze_recipe_image)
|
||||||
app.router.add_post('/api/recipes/download-missing-loras', routes.download_missing_loras)
|
|
||||||
app.router.add_post('/api/recipes/save', routes.save_recipe)
|
app.router.add_post('/api/recipes/save', routes.save_recipe)
|
||||||
|
|
||||||
# Start cache initialization
|
# Start cache initialization
|
||||||
@@ -221,15 +220,12 @@ 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 = ""
|
|
||||||
|
|
||||||
if civitai_info and 'files' in civitai_info and civitai_info['files']:
|
if civitai_info and 'files' in civitai_info and civitai_info['files']:
|
||||||
sha256 = civitai_info['files'][0].get('hashes', {}).get('SHA256', '')
|
sha256 = civitai_info['files'][0].get('hashes', {}).get('SHA256', '')
|
||||||
if sha256:
|
if sha256:
|
||||||
sha256 = sha256.lower() # Convert to lowercase for consistency
|
sha256 = sha256.lower() # Convert to lowercase for consistency
|
||||||
exists_locally = self.recipe_scanner._lora_scanner.has_lora_hash(sha256)
|
exists_locally = self.recipe_scanner._lora_scanner.has_lora_hash(sha256)
|
||||||
if exists_locally:
|
|
||||||
local_path = self.recipe_scanner._lora_scanner.get_lora_path_by_hash(sha256) or ""
|
|
||||||
|
|
||||||
# Create LoRA entry
|
# Create LoRA entry
|
||||||
lora_entry = {
|
lora_entry = {
|
||||||
@@ -239,7 +235,6 @@ class RecipeRoutes:
|
|||||||
'type': resource.get('type', 'lora'),
|
'type': resource.get('type', 'lora'),
|
||||||
'weight': resource.get('weight', 1.0),
|
'weight': resource.get('weight', 1.0),
|
||||||
'existsLocally': exists_locally,
|
'existsLocally': exists_locally,
|
||||||
'localPath': local_path,
|
|
||||||
'thumbnailUrl': '',
|
'thumbnailUrl': '',
|
||||||
'baseModel': '',
|
'baseModel': '',
|
||||||
'size': 0,
|
'size': 0,
|
||||||
@@ -283,62 +278,6 @@ class RecipeRoutes:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting temporary file: {e}")
|
logger.error(f"Error deleting temporary file: {e}")
|
||||||
|
|
||||||
async def download_missing_loras(self, request: web.Request) -> web.Response:
|
|
||||||
"""Download missing LoRAs for a recipe"""
|
|
||||||
try:
|
|
||||||
data = await request.json()
|
|
||||||
loras = data.get('loras', [])
|
|
||||||
lora_root = data.get('lora_root', '')
|
|
||||||
relative_path = data.get('relative_path', '')
|
|
||||||
|
|
||||||
if not loras:
|
|
||||||
return web.json_response({"error": "No LoRAs specified"}, status=400)
|
|
||||||
|
|
||||||
if not lora_root:
|
|
||||||
return web.json_response({"error": "No LoRA root directory specified"}, status=400)
|
|
||||||
|
|
||||||
# Create target directory if it doesn't exist
|
|
||||||
target_dir = os.path.join(lora_root, relative_path) if relative_path else lora_root
|
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Download each LoRA
|
|
||||||
downloaded = []
|
|
||||||
for lora in loras:
|
|
||||||
download_url = lora.get('downloadUrl')
|
|
||||||
if not download_url:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Generate filename from LoRA name
|
|
||||||
filename = f"{lora.get('name', 'lora')}.safetensors"
|
|
||||||
filename = filename.replace(' ', '_').replace('/', '_').replace('\\', '_')
|
|
||||||
|
|
||||||
# Download the file
|
|
||||||
target_path = os.path.join(target_dir, filename)
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(download_url, allow_redirects=True) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
continue
|
|
||||||
|
|
||||||
with open(target_path, 'wb') as f:
|
|
||||||
while True:
|
|
||||||
chunk = await response.content.read(1024 * 1024) # 1MB chunks
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
f.write(chunk)
|
|
||||||
|
|
||||||
downloaded.append({
|
|
||||||
'id': lora.get('id'),
|
|
||||||
'localPath': target_path
|
|
||||||
})
|
|
||||||
|
|
||||||
return web.json_response({
|
|
||||||
'downloaded': downloaded
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error downloading missing LoRAs: {e}", exc_info=True)
|
|
||||||
return web.json_response({"error": str(e)}, status=500)
|
|
||||||
|
|
||||||
async def save_recipe(self, request: web.Request) -> web.Response:
|
async def save_recipe(self, request: web.Request) -> web.Response:
|
||||||
"""Save a recipe to the recipes folder"""
|
"""Save a recipe to the recipes folder"""
|
||||||
@@ -349,7 +288,7 @@ class RecipeRoutes:
|
|||||||
image = None
|
image = None
|
||||||
name = None
|
name = None
|
||||||
tags = []
|
tags = []
|
||||||
recipe_data = None
|
metadata = None
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
field = await reader.next()
|
field = await reader.next()
|
||||||
@@ -376,68 +315,60 @@ class RecipeRoutes:
|
|||||||
except:
|
except:
|
||||||
tags = []
|
tags = []
|
||||||
|
|
||||||
elif field.name == 'recipe_data':
|
elif field.name == 'metadata':
|
||||||
recipe_data_text = await field.text()
|
metadata_text = await field.text()
|
||||||
try:
|
try:
|
||||||
recipe_data = json.loads(recipe_data_text)
|
metadata = json.loads(metadata_text)
|
||||||
except:
|
except:
|
||||||
recipe_data = {}
|
metadata = {}
|
||||||
|
|
||||||
if not image or not name or not recipe_data:
|
if not image or not name or not metadata:
|
||||||
return web.json_response({"error": "Missing required fields"}, status=400)
|
return web.json_response({"error": "Missing required fields"}, status=400)
|
||||||
|
|
||||||
# Create recipes directory if it doesn't exist
|
# Create recipes directory if it doesn't exist
|
||||||
recipes_dir = os.path.join(config.loras_roots[0], "recipes")
|
recipes_dir = self.recipe_scanner.recipes_dir
|
||||||
os.makedirs(recipes_dir, exist_ok=True)
|
os.makedirs(recipes_dir, exist_ok=True)
|
||||||
|
|
||||||
# Generate filename from recipe name
|
# Generate UUID for the recipe
|
||||||
filename = f"{name}.jpg"
|
import uuid
|
||||||
filename = filename.replace(' ', '_').replace('/', '_').replace('\\', '_')
|
recipe_id = str(uuid.uuid4())
|
||||||
|
|
||||||
# Ensure filename is unique
|
|
||||||
counter = 1
|
|
||||||
base_name, ext = os.path.splitext(filename)
|
|
||||||
while os.path.exists(os.path.join(recipes_dir, filename)):
|
|
||||||
filename = f"{base_name}_{counter}{ext}"
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
# Save the image
|
# Save the image
|
||||||
target_path = os.path.join(recipes_dir, filename)
|
image_ext = ".jpg"
|
||||||
with open(target_path, 'wb') as f:
|
image_filename = f"{recipe_id}{image_ext}"
|
||||||
|
image_path = os.path.join(recipes_dir, image_filename)
|
||||||
|
with open(image_path, 'wb') as f:
|
||||||
f.write(image)
|
f.write(image)
|
||||||
|
|
||||||
# Add metadata to the image
|
# Create the recipe JSON
|
||||||
from PIL import Image
|
current_time = time.time()
|
||||||
from PIL.ExifTags import TAGS
|
recipe_data = {
|
||||||
from piexif import dump, load
|
"id": recipe_id,
|
||||||
import piexif.helper
|
"file_path": image_path,
|
||||||
|
"title": name,
|
||||||
# Prepare metadata
|
"modified": current_time,
|
||||||
metadata = {
|
"created_date": current_time,
|
||||||
'recipe_name': name,
|
"base_model": metadata.get("base_model", ""),
|
||||||
'recipe_tags': json.dumps(tags),
|
"loras": metadata.get("loras", []),
|
||||||
'recipe_data': json.dumps(recipe_data),
|
"gen_params": metadata.get("gen_params", {})
|
||||||
'created_date': str(time.time())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Write metadata to image
|
# Add tags if provided
|
||||||
img = Image.open(target_path)
|
if tags:
|
||||||
exif_dict = {"0th": {}, "Exif": {}, "GPS": {}, "1st": {}, "thumbnail": None}
|
recipe_data["tags"] = tags
|
||||||
|
|
||||||
for key, value in metadata.items():
|
|
||||||
exif_dict["0th"][piexif.ImageIFD.XPComment] = piexif.helper.UserComment.dump(
|
|
||||||
json.dumps({key: value})
|
|
||||||
)
|
|
||||||
|
|
||||||
exif_bytes = dump(exif_dict)
|
|
||||||
img.save(target_path, exif=exif_bytes)
|
|
||||||
|
|
||||||
|
# Save the recipe JSON
|
||||||
|
json_filename = f"{recipe_id}.recipe.json"
|
||||||
|
json_path = os.path.join(recipes_dir, json_filename)
|
||||||
|
with open(json_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(recipe_data, f, indent=4, ensure_ascii=False)
|
||||||
# Force refresh the recipe cache
|
# Force refresh the recipe cache
|
||||||
await self.recipe_scanner.get_cached_data(force_refresh=True)
|
await self.recipe_scanner.get_cached_data(force_refresh=True)
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'file_path': target_path
|
'recipe_id': recipe_id,
|
||||||
|
'image_path': image_path,
|
||||||
|
'json_path': json_path
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ class DownloadManager:
|
|||||||
all_folders.add(relative_path)
|
all_folders.add(relative_path)
|
||||||
cache.folders = sorted(list(all_folders), key=lambda x: x.lower())
|
cache.folders = sorted(list(all_folders), key=lambda x: x.lower())
|
||||||
|
|
||||||
|
# Update the hash index with the new LoRA entry
|
||||||
|
self.file_monitor.scanner._hash_index.add_entry(metadata_dict['sha256'], metadata_dict['file_path'])
|
||||||
|
|
||||||
# Report 100% completion
|
# Report 100% completion
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
await progress_callback(100)
|
await progress_callback(100)
|
||||||
|
|||||||
82
refs/recipe.json
Normal file
82
refs/recipe.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"file_path": "D:/Workspace/ComfyUI/models/loras/recipes/3.jpg",
|
||||||
|
"title": "3",
|
||||||
|
"modified": 1741837612.3931093,
|
||||||
|
"created_date": 1741492786.5581934,
|
||||||
|
"base_model": "Flux.1 D",
|
||||||
|
"loras": [
|
||||||
|
{
|
||||||
|
"file_name": "ChronoDivinitiesFlux_r1",
|
||||||
|
"hash": "ddbc5abd00db46ad464f5e3ca85f8f7121bc14b594d6785f441d9b002fffe66a",
|
||||||
|
"strength": 0.8,
|
||||||
|
"modelVersionId": 1438879,
|
||||||
|
"modelName": "Chrono Divinities - By HailoKnight",
|
||||||
|
"modelVersionName": "Flux"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "flux.1_lora_flyway_ink-dynamic",
|
||||||
|
"hash": "4b4f3b469a0d5d3a04a46886abfa33daa37a905db070ccfbd10b345c6fb00eff",
|
||||||
|
"strength": 0.2,
|
||||||
|
"modelVersionId": 914935,
|
||||||
|
"modelName": "Ink-style",
|
||||||
|
"modelVersionName": "ink-dynamic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "",
|
||||||
|
"hash": "48c67064e2936aec342580a2a729d91d75eb818e45ecf993b9650cc66c94c420",
|
||||||
|
"strength": 0.2,
|
||||||
|
"modelVersionId": 1189379,
|
||||||
|
"modelName": "Painterly Fantasy by ChronoKnight - [FLUX & IL]",
|
||||||
|
"modelVersionName": "FLUX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "RetroAnimeFluxV1",
|
||||||
|
"hash": "8f43c31b6c3238ac44195c970d511d759c5893bddd00f59f42b8fe51e8e76fa0",
|
||||||
|
"strength": 0.8,
|
||||||
|
"modelVersionId": 806265,
|
||||||
|
"modelName": "Retro Anime Flux - Style",
|
||||||
|
"modelVersionName": "v1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "",
|
||||||
|
"hash": "e6961502769123bf23a66c5c5298d76264fd6b9610f018319a0ccb091bfc308e",
|
||||||
|
"strength": 0.2,
|
||||||
|
"modelVersionId": 757030,
|
||||||
|
"modelName": "Mezzotint Artstyle for Flux - by Ethanar",
|
||||||
|
"modelVersionName": "V1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "FluxMythG0thicL1nes",
|
||||||
|
"hash": "ecb03595de62bd6183a0dd2b38bea35669fd4d509f4bbae5aa0572cfb7ef4279",
|
||||||
|
"strength": 0.4,
|
||||||
|
"modelVersionId": 1202162,
|
||||||
|
"modelName": "Velvet's Mythic Fantasy Styles | Flux + Pony + illustrious",
|
||||||
|
"modelVersionName": "Flux Gothic Lines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "Elden_Ring_-_Yoshitaka_Amano",
|
||||||
|
"hash": "c660c4c55320be7206cb6a917c59d8da3953cc07169fe10bda833a54ec0024f9",
|
||||||
|
"strength": 0.75,
|
||||||
|
"modelVersionId": 746484,
|
||||||
|
"modelName": "Elden Ring - Yoshitaka Amano",
|
||||||
|
"modelVersionName": "V1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gen_params": {
|
||||||
|
"prompt": "a mysterious, steampunk-inspired character standing in a dramatic pose. The character is dressed in a long, intricately detailed dark coat with ornate patterns, a wide-brimmed hat, and leather boots. The face is partially obscured by the hat's shadow, adding to the enigmatic aura. The background showcases a large, antique clock with Roman numerals, surrounded by dynamic lightning and ethereal white birds, enhancing the fantastical atmosphere. The color palette is dominated by dark tones with striking contrasts of white and blue lightning, creating a sense of tension and energy. The overall composition is vertical, with the character centrally positioned, exuding a sense of power and mystery. hkchrono",
|
||||||
|
"negative_prompt": "",
|
||||||
|
"checkpoint": {
|
||||||
|
"type": "checkpoint",
|
||||||
|
"modelVersionId": 691639,
|
||||||
|
"modelName": "FLUX",
|
||||||
|
"modelVersionName": "Dev"
|
||||||
|
},
|
||||||
|
"steps": "30",
|
||||||
|
"sampler": "Undefined",
|
||||||
|
"cfg_scale": "3.5",
|
||||||
|
"seed": "1472903449",
|
||||||
|
"size": "832x1216",
|
||||||
|
"clip_skip": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -395,10 +395,9 @@ export class ImportManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!result.success) {
|
if (result.success) {
|
||||||
throw new Error(result.error || 'Failed to save recipe');
|
// Handle successful save
|
||||||
}
|
console.log(`Recipe saved with ID: ${result.recipe_id}`);
|
||||||
|
|
||||||
// Show success message for recipe save
|
// Show success message for recipe save
|
||||||
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
|
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
|
||||||
|
|
||||||
@@ -494,6 +493,13 @@ export class ImportManager {
|
|||||||
resetAndReload();
|
resetAndReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Handle error
|
||||||
|
console.error(`Failed to save recipe: ${result.error}`);
|
||||||
|
// Show error message to user
|
||||||
|
showToast(result.error, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving recipe:', error);
|
console.error('Error saving recipe:', error);
|
||||||
showToast(error.message, 'error');
|
showToast(error.message, 'error');
|
||||||
|
|||||||
Reference in New Issue
Block a user