mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-23 22:22:11 -03:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09f5e2961e | ||
|
|
756ad399bf | ||
|
|
02adced7b8 | ||
|
|
9059795816 | ||
|
|
6920944724 | ||
|
|
c76b287aed | ||
|
|
f7c946778d | ||
|
|
81599b8f43 | ||
|
|
db04c349a7 | ||
|
|
e57a72d12b | ||
|
|
c88388da67 |
@@ -26,8 +26,8 @@ class LoraManagerLoader:
|
|||||||
"optional": FlexibleOptionalInputType(any_type),
|
"optional": FlexibleOptionalInputType(any_type),
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("MODEL", "CLIP", IO.STRING)
|
RETURN_TYPES = ("MODEL", "CLIP", IO.STRING, IO.STRING)
|
||||||
RETURN_NAMES = ("MODEL", "CLIP", "trigger_words")
|
RETURN_NAMES = ("MODEL", "CLIP", "trigger_words", "loaded_loras")
|
||||||
FUNCTION = "load_loras"
|
FUNCTION = "load_loras"
|
||||||
|
|
||||||
async def get_lora_info(self, lora_name):
|
async def get_lora_info(self, lora_name):
|
||||||
@@ -95,5 +95,9 @@ class LoraManagerLoader:
|
|||||||
|
|
||||||
# use ',, ' to separate trigger words for group mode
|
# use ',, ' to separate trigger words for group mode
|
||||||
trigger_words_text = ",, ".join(all_trigger_words) if all_trigger_words else ""
|
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)
|
||||||
@@ -80,7 +80,8 @@ class LoraStacker:
|
|||||||
lora_path, trigger_words = asyncio.run(self.get_lora_info(lora_name))
|
lora_path, trigger_words = asyncio.run(self.get_lora_info(lora_name))
|
||||||
|
|
||||||
# Add to stack without loading
|
# 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
|
# Add trigger words to collection
|
||||||
all_trigger_words.extend(trigger_words)
|
all_trigger_words.extend(trigger_words)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import logging
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from ..utils.model_utils import determine_base_model
|
||||||
|
|
||||||
from ..services.file_monitor import LoraFileMonitor
|
from ..services.file_monitor import LoraFileMonitor
|
||||||
from ..services.download_manager import DownloadManager
|
from ..services.download_manager import DownloadManager
|
||||||
from ..services.civitai_client import CivitaiClient
|
from ..services.civitai_client import CivitaiClient
|
||||||
@@ -351,8 +353,8 @@ class ApiRoutes:
|
|||||||
|
|
||||||
# Update model name if available
|
# Update model name if available
|
||||||
if 'model' in civitai_metadata:
|
if 'model' in civitai_metadata:
|
||||||
local_metadata['model_name'] = civitai_metadata['model'].get('name',
|
if civitai_metadata.get('model', {}).get('name'):
|
||||||
local_metadata.get('model_name'))
|
local_metadata['model_name'] = civitai_metadata['model']['name']
|
||||||
|
|
||||||
# Fetch additional model metadata (description and tags) if we have model ID
|
# Fetch additional model metadata (description and tags) if we have model ID
|
||||||
model_id = civitai_metadata['modelId']
|
model_id = civitai_metadata['modelId']
|
||||||
@@ -363,7 +365,7 @@ class ApiRoutes:
|
|||||||
local_metadata['tags'] = model_metadata.get('tags', [])
|
local_metadata['tags'] = model_metadata.get('tags', [])
|
||||||
|
|
||||||
# Update base model
|
# 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
|
# Update preview if needed
|
||||||
if not local_metadata.get('preview_url') or not os.path.exists(local_metadata['preview_url']):
|
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
|
# Check local availability for each version
|
||||||
for version in versions:
|
for version in versions:
|
||||||
for file in version.get('files', []):
|
# Find the model file (type="Model") in the files list
|
||||||
sha256 = file.get('hashes', {}).get('SHA256')
|
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:
|
if sha256:
|
||||||
file['existsLocally'] = self.scanner.has_lora_hash(sha256)
|
# Set existsLocally and localPath at the version level
|
||||||
if file['existsLocally']:
|
version['existsLocally'] = self.scanner.has_lora_hash(sha256)
|
||||||
file['localPath'] = self.scanner.get_lora_path_by_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)
|
return web.json_response(versions)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching model versions: {e}")
|
logger.error(f"Error fetching model versions: {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)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class LoraScanner:
|
|||||||
# Build hash index and tags count
|
# Build hash index and tags count
|
||||||
for lora_data in raw_data:
|
for lora_data in raw_data:
|
||||||
if 'sha256' in lora_data and 'file_path' in lora_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
|
# Count tags
|
||||||
if 'tags' in lora_data and lora_data['tags']:
|
if 'tags' in lora_data and lora_data['tags']:
|
||||||
@@ -649,15 +649,15 @@ class LoraScanner:
|
|||||||
# Add new methods for hash index functionality
|
# Add new methods for hash index functionality
|
||||||
def has_lora_hash(self, sha256: str) -> bool:
|
def has_lora_hash(self, sha256: str) -> bool:
|
||||||
"""Check if a LoRA with given hash exists"""
|
"""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]:
|
def get_lora_path_by_hash(self, sha256: str) -> Optional[str]:
|
||||||
"""Get file path for a LoRA by its hash"""
|
"""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]:
|
def get_lora_hash_by_path(self, file_path: str) -> Optional[str]:
|
||||||
"""Get hash for a LoRA by its file path"""
|
"""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
|
# Add new method to get top tags
|
||||||
async def get_top_tags(self, limit: int = 20) -> List[Dict[str, any]]:
|
async def get_top_tags(self, limit: int = 20) -> List[Dict[str, any]]:
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from .model_utils import determine_base_model
|
||||||
|
|
||||||
from .lora_metadata import extract_lora_metadata
|
from .lora_metadata import extract_lora_metadata
|
||||||
from .models import LoraMetadata
|
from .models import LoraMetadata
|
||||||
|
|
||||||
@@ -105,6 +107,12 @@ async def load_metadata(file_path: str) -> Optional[LoraMetadata]:
|
|||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
needs_update = False
|
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
|
# Compare paths without extensions
|
||||||
stored_path_base = os.path.splitext(data['file_path'])[0]
|
stored_path_base = os.path.splitext(data['file_path'])[0]
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ BASE_MODEL_MAPPING = {
|
|||||||
"sd-v2": "SD 2.0",
|
"sd-v2": "SD 2.0",
|
||||||
"flux1": "Flux.1 D",
|
"flux1": "Flux.1 D",
|
||||||
"flux.1 d": "Flux.1 D",
|
"flux.1 d": "Flux.1 D",
|
||||||
"illustrious": "IL",
|
"illustrious": "Illustrious",
|
||||||
|
"il": "Illustrious",
|
||||||
"pony": "Pony",
|
"pony": "Pony",
|
||||||
"Hunyuan Video": "Hunyuan Video"
|
"Hunyuan Video": "Hunyuan Video"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class LoraMetadata:
|
|||||||
file_path=save_path.replace(os.sep, '/'),
|
file_path=save_path.replace(os.sep, '/'),
|
||||||
size=file_info.get('sizeKB', 0) * 1024,
|
size=file_info.get('sizeKB', 0) * 1024,
|
||||||
modified=datetime.now().timestamp(),
|
modified=datetime.now().timestamp(),
|
||||||
sha256=file_info['hashes'].get('SHA256', ''),
|
sha256=file_info['hashes'].get('SHA256', '').lower(),
|
||||||
base_model=base_model,
|
base_model=base_model,
|
||||||
preview_url=None, # Will be updated after preview download
|
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
|
preview_nsfw_level=0, # Will be updated after preview download, it is decided by the nsfw level of the preview image
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-lora-manager"
|
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."
|
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"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -153,56 +153,43 @@
|
|||||||
border-top: 1px solid var(--lora-border);
|
border-top: 1px solid var(--lora-border);
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
padding-top: var(--space-2);
|
padding-top: var(--space-2);
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle switch styles */
|
|
||||||
.toggle-switch {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
cursor: pointer;
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input {
|
.update-preferences .toggle-slider {
|
||||||
opacity: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-slider {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 40px;
|
width: 50px;
|
||||||
height: 20px;
|
height: 24px;
|
||||||
background-color: var(--border-color);
|
|
||||||
border-radius: 20px;
|
|
||||||
transition: .4s;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-slider:before {
|
.update-preferences .toggle-label {
|
||||||
position: absolute;
|
margin-left: 0;
|
||||||
content: "";
|
white-space: nowrap;
|
||||||
height: 16px;
|
line-height: 24px;
|
||||||
width: 16px;
|
|
||||||
left: 2px;
|
|
||||||
bottom: 2px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: .4s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .toggle-slider {
|
@media (max-width: 480px) {
|
||||||
background-color: var(--lora-accent);
|
.update-preferences {
|
||||||
}
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
input:checked + .toggle-slider:before {
|
}
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
.update-preferences .toggle-label {
|
||||||
|
margin-top: 5px;
|
||||||
.toggle-label {
|
}
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
}
|
||||||
@@ -120,16 +120,21 @@ export class DownloadManager {
|
|||||||
versionList.innerHTML = this.versions.map(version => {
|
versionList.innerHTML = this.versions.map(version => {
|
||||||
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
|
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
|
||||||
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
|
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;
|
// Use version-level size or fallback to first file
|
||||||
const localPath = version.files[0]?.localPath;
|
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样式
|
// 更新本地状态指示器为badge样式
|
||||||
const localStatus = existsLocally ?
|
const localStatus = existsLocally ?
|
||||||
`<div class="local-badge">
|
`<div class="local-badge">
|
||||||
<i class="fas fa-check"></i> In Library
|
<i class="fas fa-check"></i> In Library
|
||||||
<div class="local-path">${localPath}</div>
|
<div class="local-path">${localPath || ''}</div>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -177,12 +182,12 @@ export class DownloadManager {
|
|||||||
this.updateNextButtonState();
|
this.updateNextButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new method to update Next button state
|
// Update this method to use version-level existsLocally
|
||||||
updateNextButtonState() {
|
updateNextButtonState() {
|
||||||
const nextButton = document.querySelector('#versionStep .primary-btn');
|
const nextButton = document.querySelector('#versionStep .primary-btn');
|
||||||
if (!nextButton) return;
|
if (!nextButton) return;
|
||||||
|
|
||||||
const existsLocally = this.currentVersion?.files[0]?.existsLocally;
|
const existsLocally = this.currentVersion?.existsLocally;
|
||||||
|
|
||||||
if (existsLocally) {
|
if (existsLocally) {
|
||||||
nextButton.disabled = true;
|
nextButton.disabled = true;
|
||||||
@@ -202,7 +207,7 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Double-check if the version exists locally
|
// Double-check if the version exists locally
|
||||||
const existsLocally = this.currentVersion.files[0]?.existsLocally;
|
const existsLocally = this.currentVersion.existsLocally;
|
||||||
if (existsLocally) {
|
if (existsLocally) {
|
||||||
showToast('This version already exists in your library', 'info');
|
showToast('This version already exists in your library', 'info');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const BASE_MODELS = {
|
|||||||
LUMINA: "Lumina",
|
LUMINA: "Lumina",
|
||||||
KOLORS: "Kolors",
|
KOLORS: "Kolors",
|
||||||
NOOBAI: "NoobAI",
|
NOOBAI: "NoobAI",
|
||||||
IL: "IL",
|
ILLUSTRIOUS: "Illustrious",
|
||||||
PONY: "Pony",
|
PONY: "Pony",
|
||||||
|
|
||||||
// Video models
|
// Video models
|
||||||
@@ -82,7 +82,7 @@ export const BASE_MODEL_CLASSES = {
|
|||||||
[BASE_MODELS.LUMINA]: "lumina",
|
[BASE_MODELS.LUMINA]: "lumina",
|
||||||
[BASE_MODELS.KOLORS]: "kolors",
|
[BASE_MODELS.KOLORS]: "kolors",
|
||||||
[BASE_MODELS.NOOBAI]: "noobai",
|
[BASE_MODELS.NOOBAI]: "noobai",
|
||||||
[BASE_MODELS.IL]: "il",
|
[BASE_MODELS.ILLUSTRIOUS]: "il",
|
||||||
[BASE_MODELS.PONY]: "pony",
|
[BASE_MODELS.PONY]: "pony",
|
||||||
|
|
||||||
// Default
|
// Default
|
||||||
|
|||||||
Reference in New Issue
Block a user