mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 22:52:12 -03:00
Refactor search functionality in Lora and Recipe scanners to utilize fuzzy matching
- Introduced a new fuzzy_match utility function for improved search accuracy across Lora and Recipe scanners. - Updated search logic in LoraScanner and RecipeScanner to leverage fuzzy matching for titles, tags, and filenames, enhancing user experience. - Removed deprecated search methods to streamline the codebase and improve maintainability. - Adjusted API routes to ensure compatibility with the new search options, including recursive search handling.
This commit is contained in:
@@ -131,7 +131,6 @@ class ApiRoutes:
|
|||||||
folder = request.query.get('folder')
|
folder = request.query.get('folder')
|
||||||
search = request.query.get('search', '').lower()
|
search = request.query.get('search', '').lower()
|
||||||
fuzzy = request.query.get('fuzzy', 'false').lower() == 'true'
|
fuzzy = request.query.get('fuzzy', 'false').lower() == 'true'
|
||||||
recursive = request.query.get('recursive', 'false').lower() == 'true'
|
|
||||||
|
|
||||||
# Parse base models filter parameter
|
# Parse base models filter parameter
|
||||||
base_models = request.query.get('base_models', '').split(',')
|
base_models = request.query.get('base_models', '').split(',')
|
||||||
@@ -141,6 +140,7 @@ class ApiRoutes:
|
|||||||
search_filename = request.query.get('search_filename', 'true').lower() == 'true'
|
search_filename = request.query.get('search_filename', 'true').lower() == 'true'
|
||||||
search_modelname = request.query.get('search_modelname', 'true').lower() == 'true'
|
search_modelname = request.query.get('search_modelname', 'true').lower() == 'true'
|
||||||
search_tags = request.query.get('search_tags', 'false').lower() == 'true'
|
search_tags = request.query.get('search_tags', 'false').lower() == 'true'
|
||||||
|
recursive = request.query.get('recursive', 'false').lower() == 'true'
|
||||||
|
|
||||||
# Validate parameters
|
# Validate parameters
|
||||||
if page < 1 or page_size < 1 or page_size > 100:
|
if page < 1 or page_size < 1 or page_size > 100:
|
||||||
@@ -165,13 +165,13 @@ class ApiRoutes:
|
|||||||
folder=folder,
|
folder=folder,
|
||||||
search=search,
|
search=search,
|
||||||
fuzzy=fuzzy,
|
fuzzy=fuzzy,
|
||||||
recursive=recursive,
|
|
||||||
base_models=base_models, # Pass base models filter
|
base_models=base_models, # Pass base models filter
|
||||||
tags=tags, # Add tags parameter
|
tags=tags, # Add tags parameter
|
||||||
search_options={
|
search_options={
|
||||||
'filename': search_filename,
|
'filename': search_filename,
|
||||||
'modelname': search_modelname,
|
'modelname': search_modelname,
|
||||||
'tags': search_tags
|
'tags': search_tags,
|
||||||
|
'recursive': recursive
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class RecipeRoutes:
|
|||||||
async def get_recipes(self, request: web.Request) -> web.Response:
|
async def get_recipes(self, request: web.Request) -> web.Response:
|
||||||
"""API endpoint for getting paginated recipes"""
|
"""API endpoint for getting paginated recipes"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"get_recipes, Request: {request}")
|
|
||||||
# Get query parameters with defaults
|
# Get query parameters with defaults
|
||||||
page = int(request.query.get('page', '1'))
|
page = int(request.query.get('page', '1'))
|
||||||
page_size = int(request.query.get('page_size', '20'))
|
page_size = int(request.query.get('page_size', '20'))
|
||||||
@@ -100,7 +99,6 @@ class RecipeRoutes:
|
|||||||
'lora_model': search_lora_model
|
'lora_model': search_lora_model
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"get_recipes, Filters: {filters}, Search Options: {search_options}")
|
|
||||||
# Get paginated data
|
# Get paginated data
|
||||||
result = await self.recipe_scanner.get_paginated_data(
|
result = await self.recipe_scanner.get_paginated_data(
|
||||||
page=page,
|
page=page,
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ from operator import itemgetter
|
|||||||
from ..config import config
|
from ..config import config
|
||||||
from ..utils.file_utils import load_metadata, get_file_info
|
from ..utils.file_utils import load_metadata, get_file_info
|
||||||
from .lora_cache import LoraCache
|
from .lora_cache import LoraCache
|
||||||
from difflib import SequenceMatcher
|
|
||||||
from .lora_hash_index import LoraHashIndex
|
from .lora_hash_index import LoraHashIndex
|
||||||
from .settings_manager import settings
|
from .settings_manager import settings
|
||||||
from ..utils.constants import NSFW_LEVELS
|
from ..utils.constants import NSFW_LEVELS
|
||||||
|
from ..utils.utils import fuzzy_match
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -132,45 +132,9 @@ class LoraScanner:
|
|||||||
folders=[]
|
folders=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
def fuzzy_match(self, text: str, pattern: str, threshold: float = 0.7) -> bool:
|
|
||||||
"""
|
|
||||||
Check if text matches pattern using fuzzy matching.
|
|
||||||
Returns True if similarity ratio is above threshold.
|
|
||||||
"""
|
|
||||||
if not pattern or not text:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Convert both to lowercase for case-insensitive matching
|
|
||||||
text = text.lower()
|
|
||||||
pattern = pattern.lower()
|
|
||||||
|
|
||||||
# Split pattern into words
|
|
||||||
search_words = pattern.split()
|
|
||||||
|
|
||||||
# Check each word
|
|
||||||
for word in search_words:
|
|
||||||
# First check if word is a substring (faster)
|
|
||||||
if word in text:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If not found as substring, try fuzzy matching
|
|
||||||
# Check if any part of the text matches this word
|
|
||||||
found_match = False
|
|
||||||
for text_part in text.split():
|
|
||||||
ratio = SequenceMatcher(None, text_part, word).ratio()
|
|
||||||
if ratio >= threshold:
|
|
||||||
found_match = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found_match:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# All words found either as substrings or fuzzy matches
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'name',
|
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'name',
|
||||||
folder: str = None, search: str = None, fuzzy: bool = False,
|
folder: str = None, search: str = None, fuzzy: bool = False,
|
||||||
recursive: bool = False, base_models: list = None, tags: list = None,
|
base_models: list = None, tags: list = None,
|
||||||
search_options: dict = None) -> Dict:
|
search_options: dict = None) -> Dict:
|
||||||
"""Get paginated and filtered lora data
|
"""Get paginated and filtered lora data
|
||||||
|
|
||||||
@@ -181,10 +145,9 @@ class LoraScanner:
|
|||||||
folder: Filter by folder path
|
folder: Filter by folder path
|
||||||
search: Search term
|
search: Search term
|
||||||
fuzzy: Use fuzzy matching for search
|
fuzzy: Use fuzzy matching for search
|
||||||
recursive: Include subfolders when folder filter is applied
|
|
||||||
base_models: List of base models to filter by
|
base_models: List of base models to filter by
|
||||||
tags: List of tags to filter by
|
tags: List of tags to filter by
|
||||||
search_options: Dictionary with search options (filename, modelname, tags)
|
search_options: Dictionary with search options (filename, modelname, tags, recursive)
|
||||||
"""
|
"""
|
||||||
cache = await self.get_cached_data()
|
cache = await self.get_cached_data()
|
||||||
|
|
||||||
@@ -193,7 +156,8 @@ class LoraScanner:
|
|||||||
search_options = {
|
search_options = {
|
||||||
'filename': True,
|
'filename': True,
|
||||||
'modelname': True,
|
'modelname': True,
|
||||||
'tags': False
|
'tags': False,
|
||||||
|
'recursive': False
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get the base data set
|
# Get the base data set
|
||||||
@@ -208,7 +172,7 @@ class LoraScanner:
|
|||||||
|
|
||||||
# Apply folder filtering
|
# Apply folder filtering
|
||||||
if folder is not None:
|
if folder is not None:
|
||||||
if recursive:
|
if search_options.get('recursive', False):
|
||||||
# Recursive mode: match all paths starting with this folder
|
# Recursive mode: match all paths starting with this folder
|
||||||
filtered_data = [
|
filtered_data = [
|
||||||
item for item in filtered_data
|
item for item in filtered_data
|
||||||
@@ -237,16 +201,47 @@ class LoraScanner:
|
|||||||
|
|
||||||
# Apply search filtering
|
# Apply search filtering
|
||||||
if search:
|
if search:
|
||||||
if fuzzy:
|
search_results = []
|
||||||
filtered_data = [
|
for item in filtered_data:
|
||||||
item for item in filtered_data
|
# Check filename if enabled
|
||||||
if self._fuzzy_search_match(item, search, search_options)
|
if search_options.get('filename', True):
|
||||||
]
|
if fuzzy:
|
||||||
else:
|
if fuzzy_match(item.get('file_name', ''), search):
|
||||||
filtered_data = [
|
search_results.append(item)
|
||||||
item for item in filtered_data
|
continue
|
||||||
if self._exact_search_match(item, search, search_options)
|
else:
|
||||||
]
|
if search.lower() in item.get('file_name', '').lower():
|
||||||
|
search_results.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check model name if enabled
|
||||||
|
if search_options.get('modelname', True):
|
||||||
|
if fuzzy:
|
||||||
|
if fuzzy_match(item.get('model_name', ''), search):
|
||||||
|
search_results.append(item)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if search.lower() in item.get('model_name', '').lower():
|
||||||
|
search_results.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check tags if enabled
|
||||||
|
if search_options.get('tags', False) and item.get('tags'):
|
||||||
|
found_tag = False
|
||||||
|
for tag in item['tags']:
|
||||||
|
if fuzzy:
|
||||||
|
if fuzzy_match(tag, search):
|
||||||
|
found_tag = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if search.lower() in tag.lower():
|
||||||
|
found_tag = True
|
||||||
|
break
|
||||||
|
if found_tag:
|
||||||
|
search_results.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
filtered_data = search_results
|
||||||
|
|
||||||
# Calculate pagination
|
# Calculate pagination
|
||||||
total_items = len(filtered_data)
|
total_items = len(filtered_data)
|
||||||
@@ -263,44 +258,6 @@ class LoraScanner:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _fuzzy_search_match(self, item: Dict, search: str, search_options: Dict) -> bool:
|
|
||||||
"""Check if an item matches the search term using fuzzy matching with search options"""
|
|
||||||
# Check filename if enabled
|
|
||||||
if search_options.get('filename', True) and self.fuzzy_match(item.get('file_name', ''), search):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check model name if enabled
|
|
||||||
if search_options.get('modelname', True) and self.fuzzy_match(item.get('model_name', ''), search):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check tags if enabled
|
|
||||||
if search_options.get('tags', False) and item.get('tags'):
|
|
||||||
for tag in item['tags']:
|
|
||||||
if self.fuzzy_match(tag, search):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _exact_search_match(self, item: Dict, search: str, search_options: Dict) -> bool:
|
|
||||||
"""Check if an item matches the search term using exact matching with search options"""
|
|
||||||
search = search.lower()
|
|
||||||
|
|
||||||
# Check filename if enabled
|
|
||||||
if search_options.get('filename', True) and search in item.get('file_name', '').lower():
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check model name if enabled
|
|
||||||
if search_options.get('modelname', True) and search in item.get('model_name', '').lower():
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check tags if enabled
|
|
||||||
if search_options.get('tags', False) and item.get('tags'):
|
|
||||||
for tag in item['tags']:
|
|
||||||
if search in tag.lower():
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def invalidate_cache(self):
|
def invalidate_cache(self):
|
||||||
"""Invalidate the current cache"""
|
"""Invalidate the current cache"""
|
||||||
self._cache = None
|
self._cache = None
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..config import config
|
|||||||
from .recipe_cache import RecipeCache
|
from .recipe_cache import RecipeCache
|
||||||
from .lora_scanner import LoraScanner
|
from .lora_scanner import LoraScanner
|
||||||
from .civitai_client import CivitaiClient
|
from .civitai_client import CivitaiClient
|
||||||
|
from ..utils.utils import fuzzy_match
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -378,25 +379,26 @@ class RecipeScanner:
|
|||||||
# Build the search predicate based on search options
|
# Build the search predicate based on search options
|
||||||
def matches_search(item):
|
def matches_search(item):
|
||||||
# Search in title if enabled
|
# Search in title if enabled
|
||||||
if search_options.get('title', True) and search.lower() in str(item.get('title', '')).lower():
|
if search_options.get('title', True):
|
||||||
return True
|
if fuzzy_match(str(item.get('title', '')), search):
|
||||||
|
return True
|
||||||
|
|
||||||
# Search in tags if enabled
|
# Search in tags if enabled
|
||||||
if search_options.get('tags', True) and 'tags' in item:
|
if search_options.get('tags', True) and 'tags' in item:
|
||||||
for tag in item['tags']:
|
for tag in item['tags']:
|
||||||
if search.lower() in tag.lower():
|
if fuzzy_match(tag, search):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Search in lora file names if enabled
|
# Search in lora file names if enabled
|
||||||
if search_options.get('lora_name', True) and 'loras' in item:
|
if search_options.get('lora_name', True) and 'loras' in item:
|
||||||
for lora in item['loras']:
|
for lora in item['loras']:
|
||||||
if search.lower() in str(lora.get('file_name', '')).lower():
|
if fuzzy_match(str(lora.get('file_name', '')), search):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Search in lora model names if enabled
|
# Search in lora model names if enabled
|
||||||
if search_options.get('lora_model', True) and 'loras' in item:
|
if search_options.get('lora_model', True) and 'loras' in item:
|
||||||
for lora in item['loras']:
|
for lora in item['loras']:
|
||||||
if search.lower() in str(lora.get('modelName', '')).lower():
|
if fuzzy_match(str(lora.get('modelName', '')), search):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# No match found
|
# No match found
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from difflib import SequenceMatcher
|
||||||
import requests
|
import requests
|
||||||
import tempfile
|
import tempfile
|
||||||
import re
|
import re
|
||||||
@@ -39,3 +40,39 @@ def download_twitter_image(url):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error downloading twitter image: {e}")
|
print(f"Error downloading twitter image: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def fuzzy_match(text: str, pattern: str, threshold: float = 0.7) -> bool:
|
||||||
|
"""
|
||||||
|
Check if text matches pattern using fuzzy matching.
|
||||||
|
Returns True if similarity ratio is above threshold.
|
||||||
|
"""
|
||||||
|
if not pattern or not text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Convert both to lowercase for case-insensitive matching
|
||||||
|
text = text.lower()
|
||||||
|
pattern = pattern.lower()
|
||||||
|
|
||||||
|
# Split pattern into words
|
||||||
|
search_words = pattern.split()
|
||||||
|
|
||||||
|
# Check each word
|
||||||
|
for word in search_words:
|
||||||
|
# First check if word is a substring (faster)
|
||||||
|
if word in text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If not found as substring, try fuzzy matching
|
||||||
|
# Check if any part of the text matches this word
|
||||||
|
found_match = False
|
||||||
|
for text_part in text.split():
|
||||||
|
ratio = SequenceMatcher(None, text_part, word).ratio()
|
||||||
|
if ratio >= threshold:
|
||||||
|
found_match = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found_match:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# All words found either as substrings or fuzzy matches
|
||||||
|
return True
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
|||||||
// Clear grid if resetting
|
// Clear grid if resetting
|
||||||
const grid = document.getElementById('loraGrid');
|
const grid = document.getElementById('loraGrid');
|
||||||
if (grid) grid.innerHTML = '';
|
if (grid) grid.innerHTML = '';
|
||||||
|
initializeInfiniteScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -26,12 +27,8 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
|||||||
sort_by: pageState.sortBy
|
sort_by: pageState.sortBy
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use pageState instead of state
|
|
||||||
const isRecursiveSearch = pageState.searchOptions?.recursive ?? false;
|
|
||||||
|
|
||||||
if (pageState.activeFolder !== null) {
|
if (pageState.activeFolder !== null) {
|
||||||
params.append('folder', pageState.activeFolder);
|
params.append('folder', pageState.activeFolder);
|
||||||
params.append('recursive', isRecursiveSearch.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add search parameters if there's a search term
|
// Add search parameters if there's a search term
|
||||||
@@ -44,6 +41,7 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
|||||||
params.append('search_filename', pageState.searchOptions.filename.toString());
|
params.append('search_filename', pageState.searchOptions.filename.toString());
|
||||||
params.append('search_modelname', pageState.searchOptions.modelname.toString());
|
params.append('search_modelname', pageState.searchOptions.modelname.toString());
|
||||||
params.append('search_tags', (pageState.searchOptions.tags || false).toString());
|
params.append('search_tags', (pageState.searchOptions.tags || false).toString());
|
||||||
|
params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,8 +131,8 @@ class RecipeManager {
|
|||||||
// Update recipes grid
|
// Update recipes grid
|
||||||
this.updateRecipesGrid(data, resetPage);
|
this.updateRecipesGrid(data, resetPage);
|
||||||
|
|
||||||
// Update pagination state
|
// Update pagination state based on current page and total pages
|
||||||
this.pageState.hasMore = data.has_more || false;
|
this.pageState.hasMore = data.page < data.total_pages;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading recipes:', error);
|
console.error('Error loading recipes:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user