Merge pull request #743 from willmiao/sort-by-usage-count

Sort by usage count
This commit is contained in:
pixelpaws
2025-12-26 22:51:29 +08:00
committed by GitHub
18 changed files with 140 additions and 14 deletions

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "Update",
"updateAvailable": "Update verfügbar"
},
"usage": {
"timesUsed": "Verwendungsanzahl"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "Älteste",
"size": "Dateigröße",
"sizeDesc": "Größte",
"sizeAsc": "Kleinste"
"sizeAsc": "Kleinste",
"usage": "Anzahl Nutzung",
"usageDesc": "Meiste",
"usageAsc": "Wenigste"
},
"refresh": {
"title": "Modelliste aktualisieren",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "Update",
"updateAvailable": "Update available"
},
"usage": {
"timesUsed": "Times used"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "Oldest",
"size": "File Size",
"sizeDesc": "Largest",
"sizeAsc": "Smallest"
"sizeAsc": "Smallest",
"usage": "Use Count",
"usageDesc": "Most",
"usageAsc": "Least"
},
"refresh": {
"title": "Refresh model list",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "Actualización",
"updateAvailable": "Actualización disponible"
},
"usage": {
"timesUsed": "Veces usado"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "Más antiguo",
"size": "Tamaño de archivo",
"sizeDesc": "Mayor",
"sizeAsc": "Menor"
"sizeAsc": "Menor",
"usage": "Número de usos",
"usageDesc": "Más",
"usageAsc": "Menos"
},
"refresh": {
"title": "Actualizar lista de modelos",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "Mise à jour",
"updateAvailable": "Mise à jour disponible"
},
"usage": {
"timesUsed": "Nombre d'utilisations"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "Plus ancien",
"size": "Taille du fichier",
"sizeDesc": "Plus grand",
"sizeAsc": "Plus petit"
"sizeAsc": "Plus petit",
"usage": "Nombre d'utilisations",
"usageDesc": "Plus",
"usageAsc": "Moins"
},
"refresh": {
"title": "Actualiser la liste des modèles",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "עדכון",
"updateAvailable": "עדכון זמין"
},
"usage": {
"timesUsed": "מספר שימושים"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "הישן ביותר",
"size": "גודל קובץ",
"sizeDesc": "הגדול ביותר",
"sizeAsc": "הקטן ביותר"
"sizeAsc": "הקטן ביותר",
"usage": "מספר שימושים",
"usageDesc": "הכי הרבה",
"usageAsc": "הכי פחות"
},
"refresh": {
"title": "רענן רשימת מודלים",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "アップデート",
"updateAvailable": "アップデートがあります"
},
"usage": {
"timesUsed": "使用回数"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "古い順",
"size": "ファイルサイズ",
"sizeDesc": "大きい順",
"sizeAsc": "小さい順"
"sizeAsc": "小さい順",
"usage": "使用回数",
"usageDesc": "多い",
"usageAsc": "少ない"
},
"refresh": {
"title": "モデルリストを更新",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "업데이트",
"updateAvailable": "업데이트 가능"
},
"usage": {
"timesUsed": "사용 횟수"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "오래된순",
"size": "파일 크기",
"sizeDesc": "큰 순서",
"sizeAsc": "작은 순서"
"sizeAsc": "작은 순서",
"usage": "사용 횟수",
"usageDesc": "많은 순",
"usageAsc": "적은 순"
},
"refresh": {
"title": "모델 목록 새로고침",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "Обновление",
"updateAvailable": "Доступно обновление"
},
"usage": {
"timesUsed": "Количество использований"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "Старейшим",
"size": "Размеру файла",
"sizeDesc": "Наибольшим",
"sizeAsc": "Наименьшим"
"sizeAsc": "Наименьшим",
"usage": "Число использований",
"usageDesc": "Больше",
"usageAsc": "Меньше"
},
"refresh": {
"title": "Обновить список моделей",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "更新",
"updateAvailable": "有可用更新"
},
"usage": {
"timesUsed": "使用次数"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "最旧",
"size": "文件大小",
"sizeDesc": "最大",
"sizeAsc": "最小"
"sizeAsc": "最小",
"usage": "使用次数",
"usageDesc": "最多",
"usageAsc": "最少"
},
"refresh": {
"title": "刷新模型列表",

View File

@@ -131,6 +131,9 @@
"badges": {
"update": "更新",
"updateAvailable": "有可用更新"
},
"usage": {
"timesUsed": "使用次數"
}
},
"globalContextMenu": {
@@ -451,7 +454,10 @@
"dateAsc": "最舊",
"size": "檔案大小",
"sizeDesc": "最大",
"sizeAsc": "最小"
"sizeAsc": "最小",
"usage": "使用次數",
"usageDesc": "最多",
"usageAsc": "最少"
},
"refresh": {
"title": "重新整理模型列表",

View File

@@ -8,6 +8,7 @@ import time
from ..utils.constants import VALID_LORA_TYPES
from ..utils.models import BaseModelMetadata
from ..utils.metadata_manager import MetadataManager
from ..utils.usage_stats import UsageStats
from .model_query import (
FilterCriteria,
ModelCacheRepository,
@@ -83,9 +84,11 @@ class BaseModelService(ABC):
overall_start = time.perf_counter()
sort_params = self.cache_repository.parse_sort(sort_by)
t0 = time.perf_counter()
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)
fetch_duration = time.perf_counter() - t0
initial_count = len(sorted_data)
@@ -157,6 +160,37 @@ class BaseModelService(ABC):
)
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]:
"""Apply hash-based filtering"""

View File

@@ -35,6 +35,7 @@ class CheckpointService(BaseModelService):
"modified": checkpoint_data.get("modified", ""),
"tags": checkpoint_data.get("tags", []),
"from_civitai": checkpoint_data.get("from_civitai", True),
"usage_count": checkpoint_data.get("usage_count", 0),
"notes": checkpoint_data.get("notes", ""),
"model_type": checkpoint_data.get("model_type", "checkpoint"),
"favorite": checkpoint_data.get("favorite", False),

View File

@@ -35,6 +35,7 @@ class EmbeddingService(BaseModelService):
"modified": embedding_data.get("modified", ""),
"tags": embedding_data.get("tags", []),
"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", ""),
"model_type": embedding_data.get("model_type", "embedding"),
"favorite": embedding_data.get("favorite", False),

View File

@@ -35,6 +35,7 @@ class LoraService(BaseModelService):
"modified": lora_data.get("modified", ""),
"tags": lora_data.get("tags", []),
"from_civitai": lora_data.get("from_civitai", True),
"usage_count": lora_data.get("usage_count", 0),
"usage_tips": lora_data.get("usage_tips", ""),
"notes": lora_data.get("notes", ""),
"favorite": lora_data.get("favorite", False),

View File

@@ -17,7 +17,10 @@ SUPPORTED_SORT_MODES = [
('date', 'desc'),
('size', 'asc'),
('size', 'desc'),
('usage', 'asc'),
('usage', 'desc'),
]
# Is this in use?
DISPLAY_NAME_MODES = {"model_name", "file_name"}
@@ -239,6 +242,16 @@ class ModelCache:
key=itemgetter('size'),
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:
# Fallback: no sort
result = list(data)

View File

@@ -430,12 +430,18 @@ export function createModelCard(model, modelType) {
card.dataset.modified = model.modified;
card.dataset.file_size = model.file_size;
card.dataset.from_civitai = model.from_civitai;
card.dataset.usage_count = String(model.usage_count);
card.dataset.notes = model.notes || '';
card.dataset.base_model = model.base_model || 'Unknown';
card.dataset.favorite = model.favorite ? 'true' : 'false';
const hasUpdateAvailable = Boolean(model.update_available);
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 modelId = civitaiData?.modelId ?? civitaiData?.model_id;
if (modelId !== undefined && modelId !== null && modelId !== '') {
@@ -610,7 +616,10 @@ export function createModelCard(model, modelType) {
<div class="card-footer">
<div class="model-info">
<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 class="card-actions">
<i class="${footerActionIcon}"

View File

@@ -61,7 +61,8 @@
{% block head_scripts %}{% endblock %}
</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 -->
{% include 'components/header.html' %}

View File

@@ -15,6 +15,12 @@
<option value="size:desc">{{ t('loras.controls.sort.sizeDesc') }}</option>
<option value="size:asc">{{ t('loras.controls.sort.sizeAsc') }}</option>
</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>
</div>
<div title="{{ t('loras.controls.refresh.title') }}" class="control-group dropdown-group">