checkpoint

This commit is contained in:
Will Miao
2025-03-15 05:29:25 +08:00
parent 5e4d2c7760
commit 9bb4d7078e
4 changed files with 210 additions and 188 deletions

View File

@@ -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:

View File

@@ -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
View 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"
}
}

View File

@@ -395,103 +395,109 @@ 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
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
// Show success message for recipe save // Check if we need to download LoRAs
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success'); if (this.missingLoras.length > 0) {
// For download, we need to validate the target path
const loraRoot = document.getElementById('importLoraRoot')?.value;
if (!loraRoot) {
throw new Error('Please select a LoRA root directory');
}
// Check if we need to download LoRAs // Build target path
if (this.missingLoras.length > 0) { let targetPath = loraRoot;
// For download, we need to validate the target path if (this.selectedFolder) {
const loraRoot = document.getElementById('importLoraRoot')?.value; targetPath += '/' + this.selectedFolder;
if (!loraRoot) { }
throw new Error('Please select a LoRA root directory');
}
// Build target path const newFolder = document.getElementById('importNewFolder')?.value?.trim();
let targetPath = loraRoot; if (newFolder) {
if (this.selectedFolder) { targetPath += '/' + newFolder;
targetPath += '/' + this.selectedFolder; }
}
const newFolder = document.getElementById('importNewFolder')?.value?.trim(); // Set up WebSocket for progress updates
if (newFolder) { const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
targetPath += '/' + newFolder; const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
}
// Set up WebSocket for progress updates // Download missing LoRAs sequentially
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; this.loadingManager.show('Downloading LoRAs...', 0);
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
// Download missing LoRAs sequentially let completedDownloads = 0;
this.loadingManager.show('Downloading LoRAs...', 0); for (let i = 0; i < this.missingLoras.length; i++) {
const lora = this.missingLoras[i];
let completedDownloads = 0; // Update overall progress
for (let i = 0; i < this.missingLoras.length; i++) { this.loadingManager.setStatus(`Downloading LoRA ${i+1}/${this.missingLoras.length}: ${lora.name}`);
const lora = this.missingLoras[i];
// Update overall progress // Set up progress tracking for current download
this.loadingManager.setStatus(`Downloading LoRA ${i+1}/${this.missingLoras.length}: ${lora.name}`); ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.status === 'progress') {
// Calculate overall progress: completed files + current file progress
const overallProgress = Math.floor(
(completedDownloads + data.progress/100) / this.missingLoras.length * 100
);
this.loadingManager.setProgress(overallProgress);
}
};
// Set up progress tracking for current download try {
ws.onmessage = (event) => { // Download the LoRA
const data = JSON.parse(event.data); const response = await fetch('/api/download-lora', {
if (data.status === 'progress') { method: 'POST',
// Calculate overall progress: completed files + current file progress headers: { 'Content-Type': 'application/json' },
const overallProgress = Math.floor( body: JSON.stringify({
(completedDownloads + data.progress/100) / this.missingLoras.length * 100 download_url: lora.downloadUrl,
); lora_root: loraRoot,
this.loadingManager.setProgress(overallProgress); relative_path: targetPath.replace(loraRoot + '/', '')
} })
}; });
try { if (!response.ok) {
// Download the LoRA const errorText = await response.text();
const response = await fetch('/api/download-lora', { console.error(`Failed to download LoRA ${lora.name}: ${errorText}`);
method: 'POST', // Continue with next download
headers: { 'Content-Type': 'application/json' }, } else {
body: JSON.stringify({ completedDownloads++;
download_url: lora.downloadUrl, }
lora_root: loraRoot, } catch (downloadError) {
relative_path: targetPath.replace(loraRoot + '/', '') console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
})
});
if (!response.ok) {
const errorText = await response.text();
console.error(`Failed to download LoRA ${lora.name}: ${errorText}`);
// Continue with next download // Continue with next download
} else {
completedDownloads++;
} }
} catch (downloadError) { }
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
// Continue with next download // Close WebSocket
ws.close();
// Show final completion message
if (completedDownloads === this.missingLoras.length) {
showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success');
} else {
showToast(`Downloaded ${completedDownloads} of ${this.missingLoras.length} LoRAs`, 'warning');
} }
} }
// Close WebSocket // Close modal and reload recipes
ws.close(); modalManager.closeModal('importModal');
// Show final completion message // Refresh the recipe list if needed
if (completedDownloads === this.missingLoras.length) { if (typeof refreshRecipes === 'function') {
showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success'); refreshRecipes();
} else { } else {
showToast(`Downloaded ${completedDownloads} of ${this.missingLoras.length} LoRAs`, 'warning'); // Fallback to reloading the page
resetAndReload();
} }
}
// Close modal and reload recipes
modalManager.closeModal('importModal');
// Refresh the recipe list if needed
if (typeof refreshRecipes === 'function') {
refreshRecipes();
} else { } else {
// Fallback to reloading the page // Handle error
resetAndReload(); console.error(`Failed to save recipe: ${result.error}`);
// Show error message to user
showToast(result.error, 'error');
} }
} catch (error) { } catch (error) {