Compare commits

...

11 Commits

Author SHA1 Message Date
Will Miao
09f5e2961e Bump version to 0.7.39 2025-03-15 10:58:55 +08:00
Will Miao
756ad399bf Enhance LoraManagerLoader to include formatted loaded_loras in return values, improving data output for loaded LoRAs. 2025-03-15 10:45:32 +08:00
Will Miao
02adced7b8 Fix path formatting in LoraStacker to ensure compatibility across different operating systems by replacing '/' with os.sep. 2025-03-15 10:45:16 +08:00
Will Miao
9059795816 Enhance DownloadManager to update hash index with new LoRA entries, improving file tracking during downloads. 2025-03-15 10:16:52 +08:00
Will Miao
6920944724 Refactor API and DownloadManager to utilize version-level properties for model file existence and size, improving data handling and UI responsiveness. 2025-03-15 09:56:41 +08:00
Will Miao
c76b287aed Normalize SHA256 hash handling by converting to lowercase in LoraScanner and LoraMetadata classes for consistency. 2025-03-15 09:56:28 +08:00
Will Miao
f7c946778d Bump version to 0.7.38. fix: correct LoRA naming issue when fetching data from Civitai 2025-03-14 11:23:07 +08:00
Will Miao
81599b8f43 Fix: correct LoRA naming issue when fetching data from Civitai 2025-03-14 11:22:21 +08:00
Will Miao
db04c349a7 Bump version to 0.7.37-bugfix for release preparation 2025-03-13 11:11:51 +08:00
Will Miao
e57a72d12b Fixed an issue caused by inconsistent base model name for Illustrious. It fixes https://github.com/willmiao/ComfyUI-Lora-Manager/issues/37 2025-03-13 11:00:55 +08:00
Will Miao
c88388da67 Refactor toggle switch styles for update preferences in the modal 2025-03-13 10:00:32 +08:00
12 changed files with 90 additions and 68 deletions

View File

@@ -26,8 +26,8 @@ class LoraManagerLoader:
"optional": FlexibleOptionalInputType(any_type),
}
RETURN_TYPES = ("MODEL", "CLIP", IO.STRING)
RETURN_NAMES = ("MODEL", "CLIP", "trigger_words")
RETURN_TYPES = ("MODEL", "CLIP", IO.STRING, IO.STRING)
RETURN_NAMES = ("MODEL", "CLIP", "trigger_words", "loaded_loras")
FUNCTION = "load_loras"
async def get_lora_info(self, lora_name):
@@ -95,5 +95,9 @@ class LoraManagerLoader:
# use ',, ' to separate trigger words for group mode
trigger_words_text = ",, ".join(all_trigger_words) if all_trigger_words else ""
# Format loaded_loras as <lora:lora_name:strength> separated by spaces
formatted_loras = " ".join([f"<lora:{name.split(':')[0].strip()}:{str(strength).strip()}>"
for name, strength in [item.split(':') for item in loaded_loras]])
return (model, clip, trigger_words_text)
return (model, clip, trigger_words_text, formatted_loras)

View File

@@ -80,7 +80,8 @@ class LoraStacker:
lora_path, trigger_words = asyncio.run(self.get_lora_info(lora_name))
# Add to stack without loading
stack.append((lora_path, model_strength, clip_strength))
# replace '/' with os.sep to avoid different OS path format
stack.append((lora_path.replace('/', os.sep), model_strength, clip_strength))
# Add trigger words to collection
all_trigger_words.extend(trigger_words)

View File

@@ -4,6 +4,8 @@ import logging
from aiohttp import web
from typing import Dict, List
from ..utils.model_utils import determine_base_model
from ..services.file_monitor import LoraFileMonitor
from ..services.download_manager import DownloadManager
from ..services.civitai_client import CivitaiClient
@@ -351,8 +353,8 @@ class ApiRoutes:
# Update model name if available
if 'model' in civitai_metadata:
local_metadata['model_name'] = civitai_metadata['model'].get('name',
local_metadata.get('model_name'))
if civitai_metadata.get('model', {}).get('name'):
local_metadata['model_name'] = civitai_metadata['model']['name']
# Fetch additional model metadata (description and tags) if we have model ID
model_id = civitai_metadata['modelId']
@@ -363,7 +365,7 @@ class ApiRoutes:
local_metadata['tags'] = model_metadata.get('tags', [])
# Update base model
local_metadata['base_model'] = civitai_metadata.get('baseModel')
local_metadata['base_model'] = determine_base_model(civitai_metadata.get('baseModel'))
# Update preview if needed
if not local_metadata.get('preview_url') or not os.path.exists(local_metadata['preview_url']):
@@ -527,13 +529,24 @@ class ApiRoutes:
# Check local availability for each version
for version in versions:
for file in version.get('files', []):
sha256 = file.get('hashes', {}).get('SHA256')
# Find the model file (type="Model") in the files list
model_file = next((file for file in version.get('files', [])
if file.get('type') == 'Model'), None)
if model_file:
sha256 = model_file.get('hashes', {}).get('SHA256')
if sha256:
file['existsLocally'] = self.scanner.has_lora_hash(sha256)
if file['existsLocally']:
file['localPath'] = self.scanner.get_lora_path_by_hash(sha256)
# Set existsLocally and localPath at the version level
version['existsLocally'] = self.scanner.has_lora_hash(sha256)
if version['existsLocally']:
version['localPath'] = self.scanner.get_lora_path_by_hash(sha256)
# Also set the model file size at the version level for easier access
version['modelSizeKB'] = model_file.get('sizeKB')
else:
# No model file found in this version
version['existsLocally'] = False
return web.json_response(versions)
except Exception as e:
logger.error(f"Error fetching model versions: {e}")

View File

@@ -136,6 +136,9 @@ class DownloadManager:
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:
await progress_callback(100)

View File

@@ -102,7 +102,7 @@ class LoraScanner:
# Build hash index and tags count
for lora_data in raw_data:
if 'sha256' in lora_data and 'file_path' in lora_data:
self._hash_index.add_entry(lora_data['sha256'], lora_data['file_path'])
self._hash_index.add_entry(lora_data['sha256'].lower(), lora_data['file_path'])
# Count tags
if 'tags' in lora_data and lora_data['tags']:
@@ -649,15 +649,15 @@ class LoraScanner:
# Add new methods for hash index functionality
def has_lora_hash(self, sha256: str) -> bool:
"""Check if a LoRA with given hash exists"""
return self._hash_index.has_hash(sha256)
return self._hash_index.has_hash(sha256.lower())
def get_lora_path_by_hash(self, sha256: str) -> Optional[str]:
"""Get file path for a LoRA by its hash"""
return self._hash_index.get_path(sha256)
return self._hash_index.get_path(sha256.lower())
def get_lora_hash_by_path(self, file_path: str) -> Optional[str]:
"""Get hash for a LoRA by its file path"""
return self._hash_index.get_hash(file_path)
return self._hash_index.get_hash(file_path)
# Add new method to get top tags
async def get_top_tags(self, limit: int = 20) -> List[Dict[str, any]]:

View File

@@ -4,6 +4,8 @@ import hashlib
import json
from typing import Dict, Optional
from .model_utils import determine_base_model
from .lora_metadata import extract_lora_metadata
from .models import LoraMetadata
@@ -105,6 +107,12 @@ async def load_metadata(file_path: str) -> Optional[LoraMetadata]:
data = json.load(f)
needs_update = False
# Check and normalize base model name
normalized_base_model = determine_base_model(data['base_model'])
if data['base_model'] != normalized_base_model:
data['base_model'] = normalized_base_model
needs_update = True
# Compare paths without extensions
stored_path_base = os.path.splitext(data['file_path'])[0]

View File

@@ -8,7 +8,8 @@ BASE_MODEL_MAPPING = {
"sd-v2": "SD 2.0",
"flux1": "Flux.1 D",
"flux.1 d": "Flux.1 D",
"illustrious": "IL",
"illustrious": "Illustrious",
"il": "Illustrious",
"pony": "Pony",
"Hunyuan Video": "Hunyuan Video"
}

View File

@@ -47,7 +47,7 @@ class LoraMetadata:
file_path=save_path.replace(os.sep, '/'),
size=file_info.get('sizeKB', 0) * 1024,
modified=datetime.now().timestamp(),
sha256=file_info['hashes'].get('SHA256', ''),
sha256=file_info['hashes'].get('SHA256', '').lower(),
base_model=base_model,
preview_url=None, # Will be updated after preview download
preview_nsfw_level=0, # Will be updated after preview download, it is decided by the nsfw level of the preview image

View File

@@ -1,7 +1,7 @@
[project]
name = "comfyui-lora-manager"
description = "LoRA Manager for ComfyUI - Access it at http://localhost:8188/loras for managing LoRA models with previews and metadata integration."
version = "0.7.37"
version = "0.7.39"
license = {file = "LICENSE"}
dependencies = [
"aiohttp",

View File

@@ -153,56 +153,43 @@
border-top: 1px solid var(--lora-border);
margin-top: var(--space-2);
padding-top: var(--space-2);
}
/* Toggle switch styles */
.toggle-switch {
display: flex;
align-items: center;
gap: 12px;
justify-content: flex-start;
}
/* Override toggle switch styles for update preferences */
.update-preferences .toggle-switch {
position: relative;
display: inline-flex;
align-items: center;
width: auto;
height: 24px;
cursor: pointer;
user-select: none;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.toggle-slider {
.update-preferences .toggle-slider {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
background-color: var(--border-color);
border-radius: 20px;
transition: .4s;
width: 50px;
height: 24px;
flex-shrink: 0;
margin-right: 10px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
border-radius: 50%;
transition: .4s;
.update-preferences .toggle-label {
margin-left: 0;
white-space: nowrap;
line-height: 24px;
}
input:checked + .toggle-slider {
background-color: var(--lora-accent);
}
input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.toggle-label {
font-size: 0.9em;
color: var(--text-color);
@media (max-width: 480px) {
.update-preferences {
flex-direction: row;
flex-wrap: wrap;
}
.update-preferences .toggle-label {
margin-top: 5px;
}
}

View File

@@ -120,16 +120,21 @@ export class DownloadManager {
versionList.innerHTML = this.versions.map(version => {
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
const fileSize = (version.files[0]?.sizeKB / 1024).toFixed(2);
const existsLocally = version.files[0]?.existsLocally;
const localPath = version.files[0]?.localPath;
// Use version-level size or fallback to first file
const fileSize = version.modelSizeKB ?
(version.modelSizeKB / 1024).toFixed(2) :
(version.files[0]?.sizeKB / 1024).toFixed(2);
// Use version-level existsLocally flag
const existsLocally = version.existsLocally;
const localPath = version.localPath;
// 更新本地状态指示器为badge样式
const localStatus = existsLocally ?
`<div class="local-badge">
<i class="fas fa-check"></i> In Library
<div class="local-path">${localPath}</div>
<div class="local-path">${localPath || ''}</div>
</div>` : '';
return `
@@ -177,12 +182,12 @@ export class DownloadManager {
this.updateNextButtonState();
}
// Add new method to update Next button state
// Update this method to use version-level existsLocally
updateNextButtonState() {
const nextButton = document.querySelector('#versionStep .primary-btn');
if (!nextButton) return;
const existsLocally = this.currentVersion?.files[0]?.existsLocally;
const existsLocally = this.currentVersion?.existsLocally;
if (existsLocally) {
nextButton.disabled = true;
@@ -202,7 +207,7 @@ export class DownloadManager {
}
// Double-check if the version exists locally
const existsLocally = this.currentVersion.files[0]?.existsLocally;
const existsLocally = this.currentVersion.existsLocally;
if (existsLocally) {
showToast('This version already exists in your library', 'info');
return;

View File

@@ -31,7 +31,7 @@ export const BASE_MODELS = {
LUMINA: "Lumina",
KOLORS: "Kolors",
NOOBAI: "NoobAI",
IL: "IL",
ILLUSTRIOUS: "Illustrious",
PONY: "Pony",
// Video models
@@ -82,7 +82,7 @@ export const BASE_MODEL_CLASSES = {
[BASE_MODELS.LUMINA]: "lumina",
[BASE_MODELS.KOLORS]: "kolors",
[BASE_MODELS.NOOBAI]: "noobai",
[BASE_MODELS.IL]: "il",
[BASE_MODELS.ILLUSTRIOUS]: "il",
[BASE_MODELS.PONY]: "pony",
// Default