mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(usage_count): sorting by usage_count + usage_count on ModelCard
This commit is contained in:
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "Älteste",
|
"dateAsc": "Älteste",
|
||||||
"size": "Dateigröße",
|
"size": "Dateigröße",
|
||||||
"sizeDesc": "Größte",
|
"sizeDesc": "Größte",
|
||||||
"sizeAsc": "Kleinste"
|
"sizeAsc": "Kleinste",
|
||||||
|
"usage": "Anzahl Nutzung",
|
||||||
|
"usageDesc": "Meiste",
|
||||||
|
"usageAsc": "Wenigste"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "Modelliste aktualisieren",
|
"title": "Modelliste aktualisieren",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "Oldest",
|
"dateAsc": "Oldest",
|
||||||
"size": "File Size",
|
"size": "File Size",
|
||||||
"sizeDesc": "Largest",
|
"sizeDesc": "Largest",
|
||||||
"sizeAsc": "Smallest"
|
"sizeAsc": "Smallest",
|
||||||
|
"usage": "Use Count",
|
||||||
|
"usageDesc": "Most",
|
||||||
|
"usageAsc": "Least"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "Refresh model list",
|
"title": "Refresh model list",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "Más antiguo",
|
"dateAsc": "Más antiguo",
|
||||||
"size": "Tamaño de archivo",
|
"size": "Tamaño de archivo",
|
||||||
"sizeDesc": "Mayor",
|
"sizeDesc": "Mayor",
|
||||||
"sizeAsc": "Menor"
|
"sizeAsc": "Menor",
|
||||||
|
"usage": "Número de usos",
|
||||||
|
"usageDesc": "Más",
|
||||||
|
"usageAsc": "Menos"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "Actualizar lista de modelos",
|
"title": "Actualizar lista de modelos",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "Plus ancien",
|
"dateAsc": "Plus ancien",
|
||||||
"size": "Taille du fichier",
|
"size": "Taille du fichier",
|
||||||
"sizeDesc": "Plus grand",
|
"sizeDesc": "Plus grand",
|
||||||
"sizeAsc": "Plus petit"
|
"sizeAsc": "Plus petit",
|
||||||
|
"usage": "Nombre d'utilisations",
|
||||||
|
"usageDesc": "Plus",
|
||||||
|
"usageAsc": "Moins"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "Actualiser la liste des modèles",
|
"title": "Actualiser la liste des modèles",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "הישן ביותר",
|
"dateAsc": "הישן ביותר",
|
||||||
"size": "גודל קובץ",
|
"size": "גודל קובץ",
|
||||||
"sizeDesc": "הגדול ביותר",
|
"sizeDesc": "הגדול ביותר",
|
||||||
"sizeAsc": "הקטן ביותר"
|
"sizeAsc": "הקטן ביותר",
|
||||||
|
"usage": "מספר שימושים",
|
||||||
|
"usageDesc": "הכי הרבה",
|
||||||
|
"usageAsc": "הכי פחות"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "רענן רשימת מודלים",
|
"title": "רענן רשימת מודלים",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "古い順",
|
"dateAsc": "古い順",
|
||||||
"size": "ファイルサイズ",
|
"size": "ファイルサイズ",
|
||||||
"sizeDesc": "大きい順",
|
"sizeDesc": "大きい順",
|
||||||
"sizeAsc": "小さい順"
|
"sizeAsc": "小さい順",
|
||||||
|
"usage": "使用回数",
|
||||||
|
"usageDesc": "多い",
|
||||||
|
"usageAsc": "少ない"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "モデルリストを更新",
|
"title": "モデルリストを更新",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "오래된순",
|
"dateAsc": "오래된순",
|
||||||
"size": "파일 크기",
|
"size": "파일 크기",
|
||||||
"sizeDesc": "큰 순서",
|
"sizeDesc": "큰 순서",
|
||||||
"sizeAsc": "작은 순서"
|
"sizeAsc": "작은 순서",
|
||||||
|
"usage": "사용 횟수",
|
||||||
|
"usageDesc": "많은 순",
|
||||||
|
"usageAsc": "적은 순"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "모델 목록 새로고침",
|
"title": "모델 목록 새로고침",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "Старейшим",
|
"dateAsc": "Старейшим",
|
||||||
"size": "Размеру файла",
|
"size": "Размеру файла",
|
||||||
"sizeDesc": "Наибольшим",
|
"sizeDesc": "Наибольшим",
|
||||||
"sizeAsc": "Наименьшим"
|
"sizeAsc": "Наименьшим",
|
||||||
|
"usage": "Число использований",
|
||||||
|
"usageDesc": "Больше",
|
||||||
|
"usageAsc": "Меньше"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "Обновить список моделей",
|
"title": "Обновить список моделей",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "最旧",
|
"dateAsc": "最旧",
|
||||||
"size": "文件大小",
|
"size": "文件大小",
|
||||||
"sizeDesc": "最大",
|
"sizeDesc": "最大",
|
||||||
"sizeAsc": "最小"
|
"sizeAsc": "最小",
|
||||||
|
"usage": "使用次数",
|
||||||
|
"usageDesc": "最多",
|
||||||
|
"usageAsc": "最少"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "刷新模型列表",
|
"title": "刷新模型列表",
|
||||||
|
|||||||
@@ -443,7 +443,10 @@
|
|||||||
"dateAsc": "最舊",
|
"dateAsc": "最舊",
|
||||||
"size": "檔案大小",
|
"size": "檔案大小",
|
||||||
"sizeDesc": "最大",
|
"sizeDesc": "最大",
|
||||||
"sizeAsc": "最小"
|
"sizeAsc": "最小",
|
||||||
|
"usage": "使用次數",
|
||||||
|
"usageDesc": "最多",
|
||||||
|
"usageAsc": "最少"
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"title": "重新整理模型列表",
|
"title": "重新整理模型列表",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import os
|
|||||||
from ..utils.constants import VALID_LORA_TYPES
|
from ..utils.constants import VALID_LORA_TYPES
|
||||||
from ..utils.models import BaseModelMetadata
|
from ..utils.models import BaseModelMetadata
|
||||||
from ..utils.metadata_manager import MetadataManager
|
from ..utils.metadata_manager import MetadataManager
|
||||||
|
from ..utils.usage_stats import UsageStats
|
||||||
from .model_query import (
|
from .model_query import (
|
||||||
FilterCriteria,
|
FilterCriteria,
|
||||||
ModelCacheRepository,
|
ModelCacheRepository,
|
||||||
@@ -81,7 +82,10 @@ class BaseModelService(ABC):
|
|||||||
"""Get paginated and filtered model data"""
|
"""Get paginated and filtered model data"""
|
||||||
|
|
||||||
sort_params = self.cache_repository.parse_sort(sort_by)
|
sort_params = self.cache_repository.parse_sort(sort_by)
|
||||||
sorted_data = await self.cache_repository.fetch_sorted(sort_params)
|
if sort_params.key == 'usage':
|
||||||
|
sorted_data = await self._fetch_with_usage_sort(sort_params)
|
||||||
|
else:
|
||||||
|
sorted_data = await self.cache_repository.fetch_sorted(sort_params)
|
||||||
|
|
||||||
if hash_filters:
|
if hash_filters:
|
||||||
filtered_data = await self._apply_hash_filters(sorted_data, hash_filters)
|
filtered_data = await self._apply_hash_filters(sorted_data, hash_filters)
|
||||||
@@ -132,6 +136,37 @@ class BaseModelService(ABC):
|
|||||||
)
|
)
|
||||||
return paginated
|
return paginated
|
||||||
|
|
||||||
|
async def _fetch_with_usage_sort(self, sort_params):
|
||||||
|
"""Fetch data sorted by usage count (desc/asc)."""
|
||||||
|
cache = await self.cache_repository.get_cache()
|
||||||
|
raw_items = cache.raw_data or []
|
||||||
|
|
||||||
|
# Map model type to usage stats bucket
|
||||||
|
bucket_map = {
|
||||||
|
'lora': 'loras',
|
||||||
|
'checkpoint': 'checkpoints',
|
||||||
|
# 'embedding': 'embeddings', # TODO: Enable when embedding usage tracking is implemented
|
||||||
|
}
|
||||||
|
bucket_key = bucket_map.get(self.model_type, '')
|
||||||
|
|
||||||
|
usage_stats = UsageStats()
|
||||||
|
stats = await usage_stats.get_stats()
|
||||||
|
usage_bucket = stats.get(bucket_key, {}) if bucket_key else {}
|
||||||
|
|
||||||
|
annotated = []
|
||||||
|
for item in raw_items:
|
||||||
|
sha = (item.get('sha256') or '').lower()
|
||||||
|
usage_info = usage_bucket.get(sha, {}) if isinstance(usage_bucket, dict) else {}
|
||||||
|
usage_count = usage_info.get('total', 0) if isinstance(usage_info, dict) else 0
|
||||||
|
annotated.append({**item, 'usage_count': usage_count})
|
||||||
|
|
||||||
|
reverse = sort_params.order == 'desc'
|
||||||
|
annotated.sort(
|
||||||
|
key=lambda x: (x.get('usage_count', 0), x.get('model_name', '').lower()),
|
||||||
|
reverse=reverse
|
||||||
|
)
|
||||||
|
return annotated
|
||||||
|
|
||||||
|
|
||||||
async def _apply_hash_filters(self, data: List[Dict], hash_filters: Dict) -> List[Dict]:
|
async def _apply_hash_filters(self, data: List[Dict], hash_filters: Dict) -> List[Dict]:
|
||||||
"""Apply hash-based filtering"""
|
"""Apply hash-based filtering"""
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class CheckpointService(BaseModelService):
|
|||||||
"modified": checkpoint_data.get("modified", ""),
|
"modified": checkpoint_data.get("modified", ""),
|
||||||
"tags": checkpoint_data.get("tags", []),
|
"tags": checkpoint_data.get("tags", []),
|
||||||
"from_civitai": checkpoint_data.get("from_civitai", True),
|
"from_civitai": checkpoint_data.get("from_civitai", True),
|
||||||
|
"usage_count": checkpoint_data.get("usage_count", 0),
|
||||||
"notes": checkpoint_data.get("notes", ""),
|
"notes": checkpoint_data.get("notes", ""),
|
||||||
"model_type": checkpoint_data.get("model_type", "checkpoint"),
|
"model_type": checkpoint_data.get("model_type", "checkpoint"),
|
||||||
"favorite": checkpoint_data.get("favorite", False),
|
"favorite": checkpoint_data.get("favorite", False),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class EmbeddingService(BaseModelService):
|
|||||||
"modified": embedding_data.get("modified", ""),
|
"modified": embedding_data.get("modified", ""),
|
||||||
"tags": embedding_data.get("tags", []),
|
"tags": embedding_data.get("tags", []),
|
||||||
"from_civitai": embedding_data.get("from_civitai", True),
|
"from_civitai": embedding_data.get("from_civitai", True),
|
||||||
|
# "usage_count": embedding_data.get("usage_count", 0), # TODO: Enable when embedding usage tracking is implemented
|
||||||
"notes": embedding_data.get("notes", ""),
|
"notes": embedding_data.get("notes", ""),
|
||||||
"model_type": embedding_data.get("model_type", "embedding"),
|
"model_type": embedding_data.get("model_type", "embedding"),
|
||||||
"favorite": embedding_data.get("favorite", False),
|
"favorite": embedding_data.get("favorite", False),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class LoraService(BaseModelService):
|
|||||||
"modified": lora_data.get("modified", ""),
|
"modified": lora_data.get("modified", ""),
|
||||||
"tags": lora_data.get("tags", []),
|
"tags": lora_data.get("tags", []),
|
||||||
"from_civitai": lora_data.get("from_civitai", True),
|
"from_civitai": lora_data.get("from_civitai", True),
|
||||||
|
"usage_count": lora_data.get("usage_count", 0),
|
||||||
"usage_tips": lora_data.get("usage_tips", ""),
|
"usage_tips": lora_data.get("usage_tips", ""),
|
||||||
"notes": lora_data.get("notes", ""),
|
"notes": lora_data.get("notes", ""),
|
||||||
"favorite": lora_data.get("favorite", False),
|
"favorite": lora_data.get("favorite", False),
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ SUPPORTED_SORT_MODES = [
|
|||||||
('date', 'desc'),
|
('date', 'desc'),
|
||||||
('size', 'asc'),
|
('size', 'asc'),
|
||||||
('size', 'desc'),
|
('size', 'desc'),
|
||||||
|
('usage', 'asc'),
|
||||||
|
('usage', 'desc'),
|
||||||
]
|
]
|
||||||
|
# Is this in use?
|
||||||
|
|
||||||
DISPLAY_NAME_MODES = {"model_name", "file_name"}
|
DISPLAY_NAME_MODES = {"model_name", "file_name"}
|
||||||
|
|
||||||
@@ -234,6 +237,16 @@ class ModelCache:
|
|||||||
key=itemgetter('size'),
|
key=itemgetter('size'),
|
||||||
reverse=reverse
|
reverse=reverse
|
||||||
)
|
)
|
||||||
|
elif sort_key == 'usage':
|
||||||
|
# Sort by usage count, fallback to 0, then name for stability
|
||||||
|
return sorted(
|
||||||
|
data,
|
||||||
|
key=lambda x: (
|
||||||
|
x.get('usage_count', 0),
|
||||||
|
self._get_display_name(x).lower()
|
||||||
|
),
|
||||||
|
reverse=reverse
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Fallback: no sort
|
# Fallback: no sort
|
||||||
return list(data)
|
return list(data)
|
||||||
|
|||||||
@@ -430,12 +430,18 @@ export function createModelCard(model, modelType) {
|
|||||||
card.dataset.modified = model.modified;
|
card.dataset.modified = model.modified;
|
||||||
card.dataset.file_size = model.file_size;
|
card.dataset.file_size = model.file_size;
|
||||||
card.dataset.from_civitai = model.from_civitai;
|
card.dataset.from_civitai = model.from_civitai;
|
||||||
|
card.dataset.usage_count = String(model.usage_count);
|
||||||
card.dataset.notes = model.notes || '';
|
card.dataset.notes = model.notes || '';
|
||||||
card.dataset.base_model = model.base_model || 'Unknown';
|
card.dataset.base_model = model.base_model || 'Unknown';
|
||||||
card.dataset.favorite = model.favorite ? 'true' : 'false';
|
card.dataset.favorite = model.favorite ? 'true' : 'false';
|
||||||
const hasUpdateAvailable = Boolean(model.update_available);
|
const hasUpdateAvailable = Boolean(model.update_available);
|
||||||
card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false';
|
card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false';
|
||||||
|
|
||||||
|
// To only show usage_count when sorting by usage.
|
||||||
|
const pageState = getCurrentPageState();
|
||||||
|
const isUsageSort = pageState?.sortBy?.startsWith('usage');
|
||||||
|
const hasUsageCount = isUsageSort && typeof model.usage_count === 'number';
|
||||||
|
|
||||||
const civitaiData = model.civitai || {};
|
const civitaiData = model.civitai || {};
|
||||||
const modelId = civitaiData?.modelId ?? civitaiData?.model_id;
|
const modelId = civitaiData?.modelId ?? civitaiData?.model_id;
|
||||||
if (modelId !== undefined && modelId !== null && modelId !== '') {
|
if (modelId !== undefined && modelId !== null && modelId !== '') {
|
||||||
@@ -610,7 +616,10 @@ export function createModelCard(model, modelType) {
|
|||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="model-info">
|
<div class="model-info">
|
||||||
<span class="model-name">${getDisplayName(model)}</span>
|
<span class="model-name">${getDisplayName(model)}</span>
|
||||||
${model.civitai?.name ? `<span class="version-name">${model.civitai.name}</span>` : ''}
|
<div>
|
||||||
|
${model.civitai?.name ? `<span class="version-name">${model.civitai.name}</span>` : ''}
|
||||||
|
${hasUsageCount ? `<span class="version-name" title="${translate('modelCard.usage.timesUsed', {}, 'Times used')}">${model.usage_count}×</span>` : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<i class="${footerActionIcon}"
|
<i class="${footerActionIcon}"
|
||||||
|
|||||||
@@ -61,7 +61,8 @@
|
|||||||
{% block head_scripts %}{% endblock %}
|
{% block head_scripts %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-page="{% block page_id %}base{% endblock %}">
|
{% set page_id = self.page_id() %}
|
||||||
|
<body data-page="{{ page_id }}">
|
||||||
<!-- Header is always visible, even during initialization -->
|
<!-- Header is always visible, even during initialization -->
|
||||||
{% include 'components/header.html' %}
|
{% include 'components/header.html' %}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
<option value="size:desc">{{ t('loras.controls.sort.sizeDesc') }}</option>
|
<option value="size:desc">{{ t('loras.controls.sort.sizeDesc') }}</option>
|
||||||
<option value="size:asc">{{ t('loras.controls.sort.sizeAsc') }}</option>
|
<option value="size:asc">{{ t('loras.controls.sort.sizeAsc') }}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
{% if page_id != 'embeddings' %}
|
||||||
|
<optgroup label="{{ t('loras.controls.sort.usage', default='Usage') }}">
|
||||||
|
<option value="usage:desc">{{ t('loras.controls.sort.usageDesc', default='Times used (high to low)') }}</option>
|
||||||
|
<option value="usage:asc">{{ t('loras.controls.sort.usageAsc', default='Times used (low to high)') }}</option>
|
||||||
|
</optgroup>
|
||||||
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div title="{{ t('loras.controls.refresh.title') }}" class="control-group dropdown-group">
|
<div title="{{ t('loras.controls.refresh.title') }}" class="control-group dropdown-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user