diff --git a/locales/de.json b/locales/de.json index a4963a3b..0b38aa3f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1616,7 +1616,7 @@ "actions": { "download": "Herunterladen", "downloadTooltip": "Diese Version herunterladen", - "downloadRemainingTooltip": "Verbleibende Dateien dieser Version herunterladen", + "downloadChooseFilesTooltip": "Auswählen, welche Dateien heruntergeladen werden sollen", "downloadEarlyAccessTooltip": "Diese Early-Access-Version von Civitai herunterladen", "downloadPaidTooltip": "Diese bezahlte Version von Civitai herunterladen", "downloadNotAllowedTooltip": "Diese Version ist nur für die On-Site-Generierung auf Civitai verfügbar", diff --git a/locales/en.json b/locales/en.json index 2e2eff37..23a46aa8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1616,7 +1616,7 @@ "actions": { "download": "Download", "downloadTooltip": "Download this version", - "downloadRemainingTooltip": "Download remaining files of this version", + "downloadChooseFilesTooltip": "Choose which files to download", "downloadEarlyAccessTooltip": "Download this early access version from Civitai", "downloadPaidTooltip": "Download this paid version from Civitai", "downloadNotAllowedTooltip": "This version is only available for on-site generation on Civitai", diff --git a/locales/es.json b/locales/es.json index 8c7ca0f7..300a41dd 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1616,7 +1616,7 @@ "actions": { "download": "Descargar", "downloadTooltip": "Descargar esta versión", - "downloadRemainingTooltip": "Descargar los archivos restantes de esta versión", + "downloadChooseFilesTooltip": "Elegir qué archivos descargar", "downloadEarlyAccessTooltip": "Descargar esta versión de acceso temprano desde Civitai", "downloadPaidTooltip": "Descargar esta versión de pago desde Civitai", "downloadNotAllowedTooltip": "Esta versión solo está disponible para generación en el sitio de Civitai", diff --git a/locales/fr.json b/locales/fr.json index a88a7570..3244bc68 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1616,7 +1616,7 @@ "actions": { "download": "Télécharger", "downloadTooltip": "Télécharger cette version", - "downloadRemainingTooltip": "Télécharger les fichiers restants de cette version", + "downloadChooseFilesTooltip": "Choisir les fichiers à télécharger", "downloadEarlyAccessTooltip": "Télécharger cette version en accès anticipé depuis Civitai", "downloadPaidTooltip": "Télécharger cette version payante depuis Civitai", "downloadNotAllowedTooltip": "Cette version n'est disponible que pour la génération sur le site Civitai", diff --git a/locales/he.json b/locales/he.json index df596458..fe43ea7e 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1616,7 +1616,7 @@ "actions": { "download": "הורדה", "downloadTooltip": "הורד את הגרסה הזו", - "downloadRemainingTooltip": "הורד את הקבצים הנותרים של גרסה זו", + "downloadChooseFilesTooltip": "בחר אילו קבצים להוריד", "downloadEarlyAccessTooltip": "הורד את גרסת ה-Early Access הזו מ-Civitai", "downloadPaidTooltip": "הורד את הגרסה בתשלום הזו מ-Civitai", "downloadNotAllowedTooltip": "גרסה זו זמינה רק ליצירה באתר Civitai", diff --git a/locales/ja.json b/locales/ja.json index 7f5bf71c..a774bfa8 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1616,7 +1616,7 @@ "actions": { "download": "ダウンロード", "downloadTooltip": "このバージョンをダウンロード", - "downloadRemainingTooltip": "このバージョンの残りのファイルをダウンロード", + "downloadChooseFilesTooltip": "ダウンロードするファイルを選択", "downloadEarlyAccessTooltip": "Civitai からこの早期アクセス版をダウンロード", "downloadPaidTooltip": "Civitai からこの有料バージョンをダウンロード", "downloadNotAllowedTooltip": "このバージョンはCivitaiサイト内でのみ利用可能で、ダウンロードはできません", diff --git a/locales/ko.json b/locales/ko.json index 343c52ee..0bda407a 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1616,7 +1616,7 @@ "actions": { "download": "다운로드", "downloadTooltip": "이 버전 다운로드", - "downloadRemainingTooltip": "이 버전의 나머지 파일 다운로드", + "downloadChooseFilesTooltip": "다운로드할 파일 선택", "downloadEarlyAccessTooltip": "Civitai에서 이 얼리 액세스 버전 다운로드", "downloadPaidTooltip": "Civitai에서 이 유료 버전 다운로드", "downloadNotAllowedTooltip": "이 버전은 Civitai 사이트 내에서만 사용 가능하며 다운로드할 수 없습니다", diff --git a/locales/ru.json b/locales/ru.json index 0964f34d..03df82f5 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1616,7 +1616,7 @@ "actions": { "download": "Скачать", "downloadTooltip": "Скачать эту версию", - "downloadRemainingTooltip": "Скачать оставшиеся файлы этой версии", + "downloadChooseFilesTooltip": "Выбрать файлы для скачивания", "downloadEarlyAccessTooltip": "Скачать эту версию раннего доступа с Civitai", "downloadPaidTooltip": "Скачать эту платную версию с Civitai", "downloadNotAllowedTooltip": "Эта версия доступна только для генерации на сайте Civitai", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 7be918bf..66ff68b2 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1616,7 +1616,7 @@ "actions": { "download": "下载", "downloadTooltip": "下载此版本", - "downloadRemainingTooltip": "下载此版本的剩余文件", + "downloadChooseFilesTooltip": "选择要下载的文件", "downloadEarlyAccessTooltip": "从 Civitai 下载此抢先体验版本", "downloadPaidTooltip": "从 Civitai 下载此付费版本", "downloadNotAllowedTooltip": "此版本仅在 Civitai 站内可用,无法下载", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index e9e4a779..d5d96348 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1616,7 +1616,7 @@ "actions": { "download": "下載", "downloadTooltip": "下載此版本", - "downloadRemainingTooltip": "下載此版本的剩餘檔案", + "downloadChooseFilesTooltip": "選擇要下載的檔案", "downloadEarlyAccessTooltip": "從 Civitai 下載此搶先體驗版本", "downloadPaidTooltip": "從 Civitai 下載此付費版本", "downloadNotAllowedTooltip": "此版本僅在 Civitai 站內可用,無法下載", diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index 22c00ee6..4f1b30a7 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -3120,6 +3120,9 @@ class ModelUpdateHandler: "paidAccess": paid_access_payload, "filePath": context.get("file_path"), "fileName": context.get("file_name"), + # Weight-file variant count (None when unknown); lets the UI hide + # the download affordance for single-file in-library versions. + "fileCount": getattr(version, "file_count", None), } async def _build_version_context( diff --git a/py/services/model_update_service.py b/py/services/model_update_service.py index 08686784..3c3b12bc 100644 --- a/py/services/model_update_service.py +++ b/py/services/model_update_service.py @@ -18,6 +18,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence from .errors import RateLimitError, ResourceNotFoundError from .settings_manager import get_settings_manager from ..utils.cache_paths import CacheType, resolve_cache_path_with_migration +from ..utils.constants import MODEL_WEIGHT_FILE_TYPES from ..utils.civitai_utils import rewrite_preview_url from ..utils.preview_selection import resolve_mature_threshold, select_preview_media @@ -77,6 +78,10 @@ class ModelVersionRecord: usage_control: Optional[str] = None # "Download", "Generation", "InternalGeneration" paid_access: Optional[str] = None # JSON string of the CivitAI paidAccess DTO is_paid: bool = False # True when paidAccess.permanent is True (permanent paid gate) + # Number of downloadable weight files for the version (None when unknown, + # e.g. records persisted before this field existed or locally-synthesized + # entries). Mirrors the frontend isModelWeightFile() filter. + file_count: Optional[int] = None @dataclass @@ -273,6 +278,7 @@ class ModelUpdateService: usage_control TEXT, paid_access TEXT, is_paid INTEGER NOT NULL DEFAULT 0, + file_count INTEGER, PRIMARY KEY (model_id, version_id), FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE ); @@ -520,6 +526,10 @@ class ModelUpdateService: "ALTER TABLE model_update_versions " "ADD COLUMN is_paid INTEGER NOT NULL DEFAULT 0" ), + "file_count": ( + "ALTER TABLE model_update_versions " + "ADD COLUMN file_count INTEGER" + ), } for column, statement in migrations.items(): @@ -623,6 +633,7 @@ class ModelUpdateService: is_early_access INTEGER NOT NULL DEFAULT 0, paid_access TEXT, is_paid INTEGER NOT NULL DEFAULT 0, + file_count INTEGER, PRIMARY KEY (model_id, version_id), FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE ) @@ -644,6 +655,7 @@ class ModelUpdateService: "is_early_access", "paid_access", "is_paid", + "file_count", ] defaults = { "sort_index": "0", @@ -658,6 +670,7 @@ class ModelUpdateService: "is_early_access": "0", "paid_access": "NULL", "is_paid": "0", + "file_count": "NULL", } select_parts = [] @@ -1504,6 +1517,7 @@ class ModelUpdateService: ) ignore_map = {version.version_id: version.should_ignore for version in existing.versions} if existing else {} preview_map = {version.version_id: version.preview_url for version in existing.versions} if existing else {} + file_count_map = {version.version_id: version.file_count for version in existing.versions} if existing else {} sort_map = {version.version_id: version.sort_index for version in existing.versions} if existing else {} existing_map = {version.version_id: version for version in existing.versions} if existing else {} @@ -1528,6 +1542,11 @@ class ModelUpdateService: usage_control=remote_version.usage_control, paid_access=remote_version.paid_access, is_paid=remote_version.is_paid, + file_count=( + remote_version.file_count + if remote_version.file_count is not None + else file_count_map.get(version_id) + ), ) ) @@ -1620,6 +1639,7 @@ class ModelUpdateService: base_model = _normalize_string(entry.get("baseModel")) released_at = _normalize_string(entry.get("publishedAt") or entry.get("createdAt")) size_bytes = self._extract_size_bytes(entry.get("files")) + file_count = self._extract_file_count(entry.get("files")) preview_url = self._extract_preview_url(entry.get("images")) early_access_ends_at = _normalize_string(entry.get("earlyAccessEndsAt")) @@ -1655,6 +1675,7 @@ class ModelUpdateService: usage_control=usage_control, paid_access=paid_access_json, is_paid=is_paid, + file_count=file_count, ) @staticmethod @@ -1683,6 +1704,25 @@ class ModelUpdateService: return None return {"permanent": permanent, "endsAt": ends_at} + @staticmethod + def _extract_file_count(files) -> Optional[int]: + """Count downloadable weight files in a version entry's ``files`` list. + + Returns None when the payload carries no files array (unknown), so + callers can distinguish "no weight files" from "no data". + """ + + if not isinstance(files, list): + return None + count = 0 + for entry in files: + if not isinstance(entry, Mapping): + continue + entry_type = entry.get("type") + if isinstance(entry_type, str) and entry_type in MODEL_WEIGHT_FILE_TYPES: + count += 1 + return count + def _extract_size_bytes(self, files) -> Optional[int]: if not isinstance(files, Iterable): return None @@ -1795,7 +1835,7 @@ class ModelUpdateService: f""" SELECT model_id, version_id, sort_index, name, base_model, released_at, size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at, - is_early_access, usage_control, paid_access, is_paid + is_early_access, usage_control, paid_access, is_paid, file_count FROM model_update_versions WHERE model_id IN ({placeholders}) ORDER BY model_id ASC, sort_index ASC, version_id ASC @@ -1826,6 +1866,7 @@ class ModelUpdateService: usage_control=row["usage_control"], paid_access=row["paid_access"], is_paid=bool(row["is_paid"]), + file_count=_normalize_int(row["file_count"]), ) ) @@ -1888,8 +1929,8 @@ class ModelUpdateService: INSERT INTO model_update_versions ( version_id, model_id, sort_index, name, base_model, released_at, size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at, - is_early_access, usage_control, paid_access, is_paid - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + is_early_access, usage_control, paid_access, is_paid, file_count + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( version.version_id, @@ -1907,6 +1948,7 @@ class ModelUpdateService: version.usage_control, paid_access_value, 1 if version.is_paid else 0, + version.file_count, ), ) conn.commit() diff --git a/static/css/components/modal/download-modal.css b/static/css/components/modal/download-modal.css index a3b69700..49f606c0 100644 --- a/static/css/components/modal/download-modal.css +++ b/static/css/components/modal/download-modal.css @@ -514,6 +514,7 @@ background: oklch(var(--lora-accent) / 0.18); color: var(--lora-accent); font-size: inherit; + font-family: inherit; font-weight: 600; cursor: pointer; transition: var(--transition-base); diff --git a/static/js/components/shared/ModelVersionsTab.js b/static/js/components/shared/ModelVersionsTab.js index 7c188a4e..f0d6c3bd 100644 --- a/static/js/components/shared/ModelVersionsTab.js +++ b/static/js/components/shared/ModelVersionsTab.js @@ -575,50 +575,60 @@ function renderRow(version, options) { const actions = []; const canDownload = isDownloadAllowed(version); const downloadIcon = isEarlyAccess ? ' ' : ''; - let downloadTitle; - if (!canDownload) { - downloadTitle = translate( - 'modals.model.versions.actions.downloadNotAllowedTooltip', - {}, - 'This version is only available for on-site generation on Civitai' - ); - } else if (version.isInLibrary) { - // In-library versions may still have undownloaded weight files; the - // download modal's file dialog decides what remains (#1058). - downloadTitle = translate( - 'modals.model.versions.actions.downloadRemainingTooltip', - {}, - 'Download remaining files of this version' - ); - } else if (isPaidPermanent(version)) { - downloadTitle = translate( - 'modals.model.versions.actions.downloadPaidTooltip', - {}, - 'Download this paid version from Civitai' - ); - } else if (isEarlyAccess) { - downloadTitle = translate( - 'modals.model.versions.actions.downloadEarlyAccessTooltip', - {}, - 'Download this early access version from Civitai' - ); - } else { - downloadTitle = translate( - 'modals.model.versions.actions.downloadTooltip', - {}, - 'Download this version' - ); - } - actions.push(buildActionButton( - downloadLabel, - canDownload ? 'version-action-primary' : 'version-action-disabled', - canDownload ? 'download' : '', - { - title: downloadTitle, - iconMarkup: downloadIcon, - disabled: !canDownload, + // The Download button always fetches the default (primary) file, keeping + // the single-file experience for users who don't care about variants. + // In-library versions hide it: their default file already exists locally, + // and multi-file versions use the "N files" badge below for the remaining + // variants instead (#1058). fileCount is null for records persisted before + // the field existed; default to the single-file behavior in that case. + const fileCount = typeof version.fileCount === 'number' ? version.fileCount : null; + const showDownload = !version.isInLibrary; + if (showDownload) { + let downloadTitle; + if (!canDownload) { + downloadTitle = translate( + 'modals.model.versions.actions.downloadNotAllowedTooltip', + {}, + 'This version is only available for on-site generation on Civitai' + ); + } else if (isPaidPermanent(version)) { + downloadTitle = translate( + 'modals.model.versions.actions.downloadPaidTooltip', + {}, + 'Download this paid version from Civitai' + ); + } else if (isEarlyAccess) { + downloadTitle = translate( + 'modals.model.versions.actions.downloadEarlyAccessTooltip', + {}, + 'Download this early access version from Civitai' + ); + } else { + downloadTitle = translate( + 'modals.model.versions.actions.downloadTooltip', + {}, + 'Download this version' + ); } - )); + actions.push(buildActionButton( + downloadLabel, + canDownload ? 'version-action-primary' : 'version-action-disabled', + canDownload ? 'download' : '', + { + title: downloadTitle, + iconMarkup: downloadIcon, + disabled: !canDownload, + } + )); + } + + // Multi-file versions get an explicit entry into the download modal's + // file-selection step, mirroring the version step's file badge (#1058). + const fileSelectionBadge = fileCount !== null && fileCount > 1 + ? `` + : ''; if (version.isInLibrary && version.filePath) { actions.push(buildActionButton( deleteLabel, @@ -696,6 +706,7 @@ function renderRow(version, options) {