From 9bb4d7078e04e36e023cbac691336ca4767abecb Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sat, 15 Mar 2025 05:29:25 +0800 Subject: [PATCH] checkpoint --- py/routes/recipe_routes.py | 141 ++++++----------------- py/services/download_manager.py | 3 + refs/recipe.json | 82 +++++++++++++ static/js/managers/ImportManager.js | 172 ++++++++++++++-------------- 4 files changed, 210 insertions(+), 188 deletions(-) create mode 100644 refs/recipe.json diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index e1d90f8d..234a1ed5 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -37,7 +37,6 @@ class RecipeRoutes: app.router.add_get('/api/recipes', routes.get_recipes) 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/download-missing-loras', routes.download_missing_loras) app.router.add_post('/api/recipes/save', routes.save_recipe) # Start cache initialization @@ -221,15 +220,12 @@ class RecipeRoutes: # Check if this LoRA exists locally by SHA256 hash exists_locally = False - local_path = "" if civitai_info and 'files' in civitai_info and civitai_info['files']: sha256 = civitai_info['files'][0].get('hashes', {}).get('SHA256', '') if sha256: sha256 = sha256.lower() # Convert to lowercase for consistency 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 lora_entry = { @@ -239,7 +235,6 @@ class RecipeRoutes: 'type': resource.get('type', 'lora'), 'weight': resource.get('weight', 1.0), 'existsLocally': exists_locally, - 'localPath': local_path, 'thumbnailUrl': '', 'baseModel': '', 'size': 0, @@ -283,62 +278,6 @@ class RecipeRoutes: except Exception as 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: """Save a recipe to the recipes folder""" @@ -349,7 +288,7 @@ class RecipeRoutes: image = None name = None tags = [] - recipe_data = None + metadata = None while True: field = await reader.next() @@ -376,68 +315,60 @@ class RecipeRoutes: except: tags = [] - elif field.name == 'recipe_data': - recipe_data_text = await field.text() + elif field.name == 'metadata': + metadata_text = await field.text() try: - recipe_data = json.loads(recipe_data_text) + metadata = json.loads(metadata_text) 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) # 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) - # Generate filename from recipe name - filename = f"{name}.jpg" - filename = filename.replace(' ', '_').replace('/', '_').replace('\\', '_') - - # 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 + # Generate UUID for the recipe + import uuid + recipe_id = str(uuid.uuid4()) # Save the image - target_path = os.path.join(recipes_dir, filename) - with open(target_path, 'wb') as f: + image_ext = ".jpg" + 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) - # Add metadata to the image - from PIL import Image - from PIL.ExifTags import TAGS - from piexif import dump, load - import piexif.helper - - # Prepare metadata - metadata = { - 'recipe_name': name, - 'recipe_tags': json.dumps(tags), - 'recipe_data': json.dumps(recipe_data), - 'created_date': str(time.time()) + # Create the recipe JSON + current_time = time.time() + recipe_data = { + "id": recipe_id, + "file_path": image_path, + "title": name, + "modified": current_time, + "created_date": current_time, + "base_model": metadata.get("base_model", ""), + "loras": metadata.get("loras", []), + "gen_params": metadata.get("gen_params", {}) } - # Write metadata to image - img = Image.open(target_path) - exif_dict = {"0th": {}, "Exif": {}, "GPS": {}, "1st": {}, "thumbnail": None} - - 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) + # Add tags if provided + if tags: + recipe_data["tags"] = tags + # 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 await self.recipe_scanner.get_cached_data(force_refresh=True) - return web.json_response({ 'success': True, - 'file_path': target_path + 'recipe_id': recipe_id, + 'image_path': image_path, + 'json_path': json_path }) except Exception as e: diff --git a/py/services/download_manager.py b/py/services/download_manager.py index 2dd0444f..b22b99ca 100644 --- a/py/services/download_manager.py +++ b/py/services/download_manager.py @@ -135,6 +135,9 @@ class DownloadManager: all_folders = set(cache.folders) all_folders.add(relative_path) 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 if progress_callback: diff --git a/refs/recipe.json b/refs/recipe.json new file mode 100644 index 00000000..2fae8487 --- /dev/null +++ b/refs/recipe.json @@ -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" + } +} \ No newline at end of file diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index f0cc40d3..065acf9d 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -395,103 +395,109 @@ export class ImportManager { }); const result = await response.json(); - if (!result.success) { - throw new Error(result.error || 'Failed to save recipe'); - } - - // Show success message for recipe save - showToast(`Recipe "${this.recipeName}" saved successfully`, 'success'); - - // Check if we need to download LoRAs - 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'); - } + if (result.success) { + // 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'); - // Build target path - let targetPath = loraRoot; - if (this.selectedFolder) { - targetPath += '/' + this.selectedFolder; - } - - const newFolder = document.getElementById('importNewFolder')?.value?.trim(); - if (newFolder) { - targetPath += '/' + newFolder; - } - - // Set up WebSocket for progress updates - const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; - const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`); - - // Download missing LoRAs sequentially - this.loadingManager.show('Downloading LoRAs...', 0); - - let completedDownloads = 0; - for (let i = 0; i < this.missingLoras.length; i++) { - const lora = this.missingLoras[i]; + // Check if we need to download LoRAs + 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'); + } - // Update overall progress - this.loadingManager.setStatus(`Downloading LoRA ${i+1}/${this.missingLoras.length}: ${lora.name}`); + // Build target path + let targetPath = loraRoot; + if (this.selectedFolder) { + targetPath += '/' + this.selectedFolder; + } - // Set up progress tracking for current download - 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); - } - }; + const newFolder = document.getElementById('importNewFolder')?.value?.trim(); + if (newFolder) { + targetPath += '/' + newFolder; + } - try { - // Download the LoRA - const response = await fetch('/api/download-lora', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - download_url: lora.downloadUrl, - lora_root: loraRoot, - relative_path: targetPath.replace(loraRoot + '/', '') - }) - }); + // Set up WebSocket for progress updates + const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; + const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`); + + // Download missing LoRAs sequentially + this.loadingManager.show('Downloading LoRAs...', 0); + + let completedDownloads = 0; + for (let i = 0; i < this.missingLoras.length; i++) { + const lora = this.missingLoras[i]; - if (!response.ok) { - const errorText = await response.text(); - console.error(`Failed to download LoRA ${lora.name}: ${errorText}`); + // Update overall progress + this.loadingManager.setStatus(`Downloading LoRA ${i+1}/${this.missingLoras.length}: ${lora.name}`); + + // Set up progress tracking for current download + 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); + } + }; + + try { + // Download the LoRA + const response = await fetch('/api/download-lora', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + download_url: lora.downloadUrl, + lora_root: loraRoot, + relative_path: targetPath.replace(loraRoot + '/', '') + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`Failed to download LoRA ${lora.name}: ${errorText}`); + // Continue with next download + } else { + completedDownloads++; + } + } catch (downloadError) { + console.error(`Error downloading LoRA ${lora.name}:`, downloadError); // 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 - ws.close(); + // Close modal and reload recipes + modalManager.closeModal('importModal'); - // Show final completion message - if (completedDownloads === this.missingLoras.length) { - showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success'); + // Refresh the recipe list if needed + if (typeof refreshRecipes === 'function') { + refreshRecipes(); } 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 { - // Fallback to reloading the page - resetAndReload(); + // Handle error + console.error(`Failed to save recipe: ${result.error}`); + // Show error message to user + showToast(result.error, 'error'); } } catch (error) {