Compare commits

..

13 Commits

Author SHA1 Message Date
Will Miao
6d8408e626 feat: update release notes and version to 0.8.26, adding creator search and enhancing node usability 2025-08-08 20:10:06 +08:00
Will Miao
0906271aa9 refactor: simplify auto download check logic by removing unnecessary progress updates 2025-08-08 19:58:20 +08:00
Will Miao
4c33c9d256 feat: enhance folder update logic with error handling in fetchModelsPage 2025-08-08 17:33:11 +08:00
Will Miao
fa9c78209f feat: update API endpoints to include '/list' for model retrieval in routes and templates, fixes #344 2025-08-07 18:06:40 +08:00
Will Miao
6678ec8a60 refactor: remove unused height properties and simplify widget height handling in various components, fixes #284 2025-08-07 16:49:39 +08:00
Will Miao
854e467c12 feat: add debug logging for default root settings in DownloadManager 2025-08-07 14:42:05 +08:00
Will Miao
e6b94c7b21 refactor: remove unused import and simplify filename handling in ModelHashIndex, fixes #342 2025-08-06 19:11:07 +08:00
Will Miao
2c6f9d8602 feat: add creator search option and update related functionality across models and UI 2025-08-06 18:32:57 +08:00
Will Miao
c74033b9c0 refactor: conditionally initialize managers in HeaderManager to avoid unnecessary setup on statistics page 2025-08-06 11:14:02 +08:00
Will Miao
d2b21d27bb refactor: remove unused imports from update_routes.py and requirements.txt 2025-08-06 10:34:40 +08:00
Will Miao
215272469f refactor: replace model API client import and remove performance logging, add reset and reload functionality 2025-08-06 07:56:48 +08:00
Will Miao
f7d05ab0f1 refactor: change logging level from info to debug for download progress messages 2025-08-06 06:44:35 +08:00
Will Miao
6f2ad2be77 fix: update LoRA model type check to use constant for improved readability, fixes #341 2025-08-05 19:11:28 +08:00
29 changed files with 77 additions and 98 deletions

View File

@@ -34,6 +34,14 @@ Enhance your Civitai browsing experience with our companion browser extension! S
## Release Notes
### v0.8.26
* **Creator Search Option**
- Added ability to search models by creator name, making it easier to find models from specific authors.
* **Enhanced Node Usability**
- Improved user experience for Lora Loader, Lora Stacker, and WanVideo Lora Select nodes by fixing the maximum height of the text input area. Users can now freely and conveniently adjust the LoRA region within these nodes.
* **Compatibility Fixes**
- Resolved compatibility issues with ComfyUI and certain custom nodes, including ComfyUI-Custom-Scripts, ensuring smoother integration and operation.
### v0.8.25
* **LoRA List Reordering**
- Drag & Drop: Easily rearrange LoRA entries using the drag handle.

View File

@@ -1,6 +1,5 @@
import json
import os
import asyncio
import re
import numpy as np
import folder_paths # type: ignore

View File

@@ -38,7 +38,7 @@ class BaseModelRoutes(ABC):
prefix: URL prefix (e.g., 'loras', 'checkpoints')
"""
# Common model management routes
app.router.add_get(f'/api/{prefix}', self.get_models)
app.router.add_get(f'/api/{prefix}/list', self.get_models)
app.router.add_post(f'/api/{prefix}/delete', self.delete_model)
app.router.add_post(f'/api/{prefix}/exclude', self.exclude_model)
app.router.add_post(f'/api/{prefix}/fetch-civitai', self.fetch_civitai)
@@ -177,6 +177,7 @@ class BaseModelRoutes(ABC):
'filename': request.query.get('search_filename', 'true').lower() == 'true',
'modelname': request.query.get('search_modelname', 'true').lower() == 'true',
'tags': request.query.get('search_tags', 'false').lower() == 'true',
'creator': request.query.get('search_creator', 'false').lower() == 'true',
'recursive': request.query.get('recursive', 'false').lower() == 'true',
}

View File

@@ -1,5 +1,4 @@
import os
import subprocess
import aiohttp
import logging
import toml
@@ -7,7 +6,6 @@ import git
import zipfile
import shutil
import tempfile
from datetime import datetime
from aiohttp import web
from typing import Dict, List

View File

@@ -199,6 +199,22 @@ class BaseModelService(ABC):
for tag in item['tags']):
search_results.append(item)
continue
# Search by creator
civitai = item.get('civitai')
creator_username = ''
if civitai and isinstance(civitai, dict):
creator = civitai.get('creator')
if creator and isinstance(creator, dict):
creator_username = creator.get('username', '')
if search_options.get('creator', False) and creator_username:
if fuzzy_search:
if fuzzy_match(creator_username, search):
search_results.append(item)
continue
elif search.lower() in creator_username.lower():
search_results.append(item)
continue
return search_results

View File

@@ -199,8 +199,6 @@ class ModelHashIndex:
def get_hash_by_filename(self, filename: str) -> Optional[str]:
"""Get hash for a filename without extension"""
# Strip extension if present to make the function more flexible
filename = os.path.splitext(filename)[0]
return self._filename_to_hash.get(filename)
def clear(self) -> None:

View File

@@ -50,7 +50,8 @@ VALID_LORA_TYPES = ['lora', 'locon', 'dora']
# Civitai model tags in priority order for subfolder organization
CIVITAI_MODEL_TAGS = [
'character', 'style', 'concept', 'clothing', 'base model',
'character', 'style', 'concept', 'clothing',
# 'base model', # exclude 'base model'
'poses', 'background', 'tool', 'vehicle', 'buildings',
'objects', 'assets', 'animal', 'action'
]

View File

@@ -91,7 +91,7 @@ class DownloadManager:
with open(progress_file, 'r', encoding='utf-8') as f:
saved_progress = json.load(f)
download_progress['processed_models'] = set(saved_progress.get('processed_models', []))
logger.info(f"Loaded previous progress, {len(download_progress['processed_models'])} models already processed")
logger.debug(f"Loaded previous progress, {len(download_progress['processed_models'])} models already processed")
except Exception as e:
logger.error(f"Failed to load progress file: {e}")
download_progress['processed_models'] = set()
@@ -230,7 +230,7 @@ class DownloadManager:
# Update total count
download_progress['total'] = len(all_models)
logger.info(f"Found {download_progress['total']} models to process")
logger.debug(f"Found {download_progress['total']} models to process")
# Process each model
for i, (scanner_type, model, scanner) in enumerate(all_models):
@@ -250,7 +250,7 @@ class DownloadManager:
# Mark as completed
download_progress['status'] = 'completed'
download_progress['end_time'] = time.time()
logger.info(f"Example images download completed: {download_progress['completed']}/{download_progress['total']} models processed")
logger.debug(f"Example images download completed: {download_progress['completed']}/{download_progress['total']} models processed")
except Exception as e:
error_msg = f"Error during example images download: {str(e)}"
@@ -307,7 +307,7 @@ class DownloadManager:
logger.debug(f"Skipping already processed model: {model_name}")
return False
else:
logger.info(f"Model {model_name} marked as processed but folder empty or missing, reprocessing")
logger.debug(f"Model {model_name} marked as processed but folder empty or missing, reprocessing")
# Create model directory
model_dir = os.path.join(output_dir, model_hash)

View File

@@ -47,7 +47,7 @@ def get_lora_info(lora_name):
# No event loop is running, we can use asyncio.run()
return asyncio.run(_get_lora_info_async())
def fuzzy_match(text: str, pattern: str, threshold: float = 0.7) -> bool:
def fuzzy_match(text: str, pattern: str, threshold: float = 0.85) -> bool:
"""
Check if text matches pattern using fuzzy matching.
Returns True if similarity ratio is above threshold.

View File

@@ -1,7 +1,7 @@
[project]
name = "comfyui-lora-manager"
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
version = "0.8.25"
version = "0.8.26"
license = {file = "LICENSE"}
dependencies = [
"aiohttp",

View File

@@ -7,5 +7,4 @@ olefile
toml
numpy
natsort
pyyaml
GitPython

View File

@@ -55,7 +55,7 @@ export function getApiEndpoints(modelType) {
return {
// Base CRUD operations
list: `/api/${modelType}`,
list: `/api/${modelType}/list`,
delete: `/api/${modelType}/delete`,
exclude: `/api/${modelType}/exclude`,
rename: `/api/${modelType}/rename`,

View File

@@ -8,7 +8,7 @@ import {
DOWNLOAD_ENDPOINTS,
WS_ENDPOINTS
} from './apiConfig.js';
import { createModelApiClient } from './modelApiFactory.js';
import { resetAndReload } from './modelApiFactory.js';
/**
* Abstract base class for all model API clients
@@ -91,10 +91,7 @@ export class BaseModelApiClient {
pageState.currentPage = 1; // Reset to first page
}
const startTime = performance.now();
const result = await this.fetchModelsPage(pageState.currentPage, pageState.pageSize);
const endTime = performance.now();
console.log(`fetchModelsPage耗时: ${(endTime - startTime).toFixed(2)} ms`);
state.virtualScroller.refreshWithData(
result.items,
@@ -105,8 +102,16 @@ export class BaseModelApiClient {
pageState.hasMore = result.hasMore;
pageState.currentPage = pageState.currentPage + 1;
if (updateFolders && result.folders) {
updateFolderTags(result.folders);
if (updateFolders) {
const response = await fetch(this.apiConfig.endpoints.folders);
if (response.ok) {
const data = await response.json();
updateFolderTags(data.folders);
} else {
const errorData = await response.json().catch(() => ({}));
const errorMsg = errorData && errorData.error ? errorData.error : response.statusText;
console.error(`Error getting folders: ${errorMsg}`);
}
}
return result;
@@ -321,6 +326,8 @@ export class BaseModelApiClient {
if (!response.ok) {
throw new Error(`Failed to refresh ${this.apiConfig.config.displayName}s: ${response.status} ${response.statusText}`);
}
resetAndReload(true);
showToast(`${fullRebuild ? 'Full rebuild' : 'Refresh'} complete`, 'success');
} catch (error) {
@@ -629,6 +636,9 @@ export class BaseModelApiClient {
if (pageState.searchOptions.tags !== undefined) {
params.append('search_tags', pageState.searchOptions.tags.toString());
}
if (pageState.searchOptions.creator !== undefined) {
params.append('search_creator', pageState.searchOptions.creator.toString());
}
params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
}
}

View File

@@ -16,7 +16,9 @@ export class HeaderManager {
this.filterManager = null;
// Initialize appropriate managers based on current page
this.initializeManagers();
if (this.currentPage !== 'statistics') {
this.initializeManagers();
}
// Set up common header functionality
this.initializeCommonElements();
@@ -37,11 +39,8 @@ export class HeaderManager {
this.searchManager = new SearchManager({ page: this.currentPage });
window.searchManager = this.searchManager;
// Initialize FilterManager for all page types that have filters
if (document.getElementById('filterButton')) {
this.filterManager = new FilterManager({ page: this.currentPage });
window.filterManager = this.filterManager;
}
this.filterManager = new FilterManager({ page: this.currentPage });
window.filterManager = this.filterManager;
}
initializeCommonElements() {

View File

@@ -241,7 +241,7 @@ function showModelModalFromCard(card, modelType) {
tags: JSON.parse(card.dataset.tags || '[]'),
modelDescription: card.dataset.modelDescription || '',
// LoRA specific fields
...(modelType === 'lora' && {
...(modelType === MODEL_TYPES.LORA && {
usage_tips: card.dataset.usage_tips,
})
};

View File

@@ -297,7 +297,10 @@ export class DownloadManager {
// Set default root if available
const defaultRootKey = `default_${this.apiClient.modelType}_root`;
const defaultRoot = getStorageItem('settings', {})[defaultRootKey];
console.log(`Default root for ${this.apiClient.modelType}:`, defaultRoot);
console.log('Available roots:', rootsData.roots);
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
console.log(`Setting default root: ${defaultRoot}`);
modelRoot.value = defaultRoot;
}

View File

@@ -763,22 +763,7 @@ class ExampleImagesManager {
const data = await response.json();
if (data.success) {
// Only show progress if there are actually items to download
if (data.status && data.status.total > 0) {
this.isDownloading = true;
this.isPaused = false;
this.hasShownCompletionToast = false;
this.startTime = new Date();
this.updateUI(data.status);
this.showProgressPanel();
this.startProgressUpdates();
this.updateDownloadButtonText();
console.log(`Auto download started: ${data.status.total} items to process`);
} else {
console.log('Auto download check completed - no new items to download');
}
} else {
if (!data.success) {
console.warn('Auto download check failed:', data.error);
}
} catch (error) {

View File

@@ -318,6 +318,7 @@ export class SearchManager {
filename: options.filename || false,
modelname: options.modelname || false,
tags: options.tags || false,
creator: options.creator || false,
recursive: recursive
};
} else if (this.currentPage === 'checkpoints') {
@@ -325,6 +326,7 @@ export class SearchManager {
filename: options.filename || false,
modelname: options.modelname || false,
tags: options.tags || false,
creator: options.creator || false,
recursive: recursive
};
}

View File

@@ -37,6 +37,7 @@ export const state = {
filename: true,
modelname: true,
tags: false,
creator: false,
recursive: false
},
filters: {
@@ -83,6 +84,7 @@ export const state = {
searchOptions: {
filename: true,
modelname: true,
creator: false,
recursive: false
},
filters: {
@@ -110,6 +112,7 @@ export const state = {
filename: true,
modelname: true,
tags: false,
creator: false,
recursive: false
},
filters: {

View File

@@ -9,7 +9,7 @@
{% block init_title %}Initializing Checkpoints Manager{% endblock %}
{% block init_message %}Scanning and building checkpoints cache. This may take a few moments...{% endblock %}
{% block init_check_url %}/api/checkpoints?page=1&page_size=1{% endblock %}
{% block init_check_url %}/api/checkpoints/list?page=1&page_size=1{% endblock %}
{% block additional_components %}

View File

@@ -86,15 +86,18 @@
<div class="search-option-tag active" data-option="filename">Filename</div>
<div class="search-option-tag active" data-option="modelname">Checkpoint Name</div>
<div class="search-option-tag active" data-option="tags">Tags</div>
<div class="search-option-tag" data-option="creator">Creator</div>
{% elif request.path == '/embeddings' %}
<div class="search-option-tag active" data-option="filename">Filename</div>
<div class="search-option-tag active" data-option="modelname">Embedding Name</div>
<div class="search-option-tag active" data-option="tags">Tags</div>
<div class="search-option-tag" data-option="creator">Creator</div>
{% else %}
<!-- Default options for LoRAs page -->
<div class="search-option-tag active" data-option="filename">Filename</div>
<div class="search-option-tag active" data-option="modelname">Model Name</div>
<div class="search-option-tag active" data-option="tags">Tags</div>
<div class="search-option-tag" data-option="creator">Creator</div>
{% endif %}
</div>
</div>

View File

@@ -9,7 +9,7 @@
{% block init_title %}Initializing Embeddings Manager{% endblock %}
{% block init_message %}Scanning and building embeddings cache. This may take a few moments...{% endblock %}
{% block init_check_url %}/api/embeddings?page=1&page_size=1{% endblock %}
{% block init_check_url %}/api/embeddings/list?page=1&page_size=1{% endblock %}
{% block additional_components %}

View File

@@ -11,7 +11,7 @@
{% block init_title %}Initializing LoRA Manager{% endblock %}
{% block init_message %}Scanning and building LoRA cache. This may take a few minutes...{% endblock %}
{% block init_check_url %}/api/loras?page=1&page_size=1{% endblock %}
{% block init_check_url %}/api/loras/list?page=1&page_size=1{% endblock %}
{% block content %}
{% include 'components/controls.html' %}

View File

@@ -5,9 +5,6 @@ export function addJsonDisplayWidget(node, name, opts) {
// Set initial height
const defaultHeight = 200;
container.style.setProperty('--comfy-widget-min-height', `${defaultHeight}px`);
container.style.setProperty('--comfy-widget-max-height', `${defaultHeight * 2}px`);
container.style.setProperty('--comfy-widget-height', `${defaultHeight}px`);
Object.assign(container.style, {
display: "block",
@@ -113,16 +110,6 @@ export function addJsonDisplayWidget(node, name, opts) {
widgetValue = v;
displayJson(widgetValue, widget);
},
getMinHeight: function() {
return parseInt(container.style.getPropertyValue('--comfy-widget-min-height')) || defaultHeight;
},
getMaxHeight: function() {
return parseInt(container.style.getPropertyValue('--comfy-widget-max-height')) || defaultHeight * 2;
},
getHeight: function() {
// Return actual container height to reduce the gap
return parseInt(container.style.getPropertyValue('--comfy-widget-height')) || defaultHeight;
},
hideOnZoom: true
});

View File

@@ -156,6 +156,7 @@ app.registerExtension({
// Update input widget callback
const inputWidget = this.widgets[0];
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
inputWidget.callback = (value) => {
if (isUpdating) return;

View File

@@ -77,6 +77,7 @@ app.registerExtension({
// Update input widget callback
const inputWidget = this.widgets[0];
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
inputWidget.callback = (value) => {
if (isUpdating) return;

View File

@@ -19,9 +19,6 @@ export function addLorasWidget(node, name, opts, callback) {
// Set initial height using CSS variables approach
const defaultHeight = 200;
container.style.setProperty('--comfy-widget-min-height', `${defaultHeight}px`);
container.style.setProperty('--comfy-widget-max-height', `${defaultHeight * 2}px`);
container.style.setProperty('--comfy-widget-height', `${defaultHeight}px`);
Object.assign(container.style, {
display: "flex",
@@ -712,23 +709,8 @@ export function addLorasWidget(node, name, opts, callback) {
widgetValue = updatedValue;
renderLoras(widgetValue, widget);
},
getMinHeight: function() {
return parseInt(container.style.getPropertyValue('--comfy-widget-min-height')) || defaultHeight;
},
getMaxHeight: function() {
return parseInt(container.style.getPropertyValue('--comfy-widget-max-height')) || defaultHeight * 2;
},
getHeight: function() {
return parseInt(container.style.getPropertyValue('--comfy-widget-height')) || defaultHeight;
},
hideOnZoom: true,
selectOn: ['click', 'focus'],
afterResize: function(node) {
// Re-render after node resize
if (this.value && this.value.length > 0) {
renderLoras(this.value, this);
}
}
selectOn: ['click', 'focus']
});
widget.value = defaultValue;

View File

@@ -5,9 +5,6 @@ export function addTagsWidget(node, name, opts, callback) {
// Set initial height
const defaultHeight = 150;
container.style.setProperty('--comfy-widget-min-height', `${defaultHeight}px`);
container.style.setProperty('--comfy-widget-max-height', `${defaultHeight * 2}px`);
container.style.setProperty('--comfy-widget-height', `${defaultHeight}px`);
Object.assign(container.style, {
display: "flex",
@@ -199,23 +196,8 @@ export function addTagsWidget(node, name, opts, callback) {
widgetValue = v;
renderTags(widgetValue, widget);
},
getMinHeight: function() {
return parseInt(container.style.getPropertyValue('--comfy-widget-min-height')) || defaultHeight;
},
getMaxHeight: function() {
return parseInt(container.style.getPropertyValue('--comfy-widget-max-height')) || defaultHeight * 2;
},
getHeight: function() {
return parseInt(container.style.getPropertyValue('--comfy-widget-height')) || defaultHeight;
},
hideOnZoom: true,
selectOn: ['click', 'focus'],
afterResize: function(node) {
// Re-render tags after node resize
if (this.value && this.value.length > 0) {
renderTags(this.value, this);
}
}
selectOn: ['click', 'focus']
});
// Set initial value

View File

@@ -78,6 +78,7 @@ app.registerExtension({
// Update input widget callback
const inputWidget = this.widgets[1];
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
inputWidget.callback = (value) => {
if (isUpdating) return;