diff --git a/docs/i18n-translation-guidelines.md b/docs/i18n-translation-guidelines.md index 052186ed..dad2d513 100644 --- a/docs/i18n-translation-guidelines.md +++ b/docs/i18n-translation-guidelines.md @@ -103,6 +103,15 @@ Locales: `en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `ru`, `he` (RTL). > failure-table conventions; reuse each locale's existing renderings of those sibling keys). > All 9 locales are translated in the same pass (terminology in §2, "Sidecar storage feature"). +> **Status (2026-10, unknown base model routing):** the download-routing inversion added +> 4 keys (`settings.unknownBaseModelRouting.label`, `.help`, `.options.diffusionModel`, +> `.options.checkpoint`) — the option labels reuse each locale's existing +> `checkpoints.modelTypes.diffusion_model` / `.checkpoint` renderings. The same pass also +> translated the leftover `doctor.issues.sidecar_mirror_orphans.title` ("Centralized +> Sidecars", §2 "Sidecar storage feature" terminology). All 9 locales are translated, +> so the "no remaining placeholders" claim holds again. Terminology in §2, "Download +> routing feature". + --- ## 1. Hard rules (do not violate) @@ -543,6 +552,25 @@ short and imperative and do not append a keyboard hint in any locale. The grip itself is an icon and is never translated. +### Download routing feature (unknown base model routing) + +The `settings.unknownBaseModelRouting.*` keys (Settings → Library → Folder Settings) decide +which library a checkpoint download lands in when CivitAI reports a baseModel that is in +neither the known-checkpoint nor the known-diffusion-model list. The option labels reuse +each locale's `checkpoints.modelTypes.diffusion_model` / `.checkpoint` renderings +(model-type names, R3 — ja/ko keep the Latin loanword), pluralized only where the locale +pluralizes (de Diffusionsmodelle, es Modelos de difusión, fr Modèles de diffusion, +ru Диффузионные модели, he מודלי דיפוזיה; CJK stays singular, "Checkpoint(s)" follows +`header.navigation.checkpoints`). + +| Term | Rendering | +|---|---| +| routing (noun, of a download into a library) | zh-CN 路由 · zh-TW 路由 · ja 振り分け · ko 라우팅 · fr routage · de Routing · es enrutamiento · ru маршрутизация · he ניתוב | +| unknown base model | zh-CN 未知基础模型 · zh-TW 未知基礎模型 · ja 不明なベースモデル · ko 알 수 없는 베이스 모델 · fr modèle de base inconnu · de unbekanntes Basismodell · es modelo base desconocido · ru неизвестная базовая модель · he מודל בסיס לא מוכר | + +The baseModel family names in the help text (`SD 1.x/2.x/3.x, SDXL, Pony, Illustrious, +NoobAI`) are CivitAI baseModel values and stay verbatim in every locale. + --- ## 3. Cross-cutting confusion hot-spots (must-fix list) diff --git a/locales/de.json b/locales/de.json index 59834869..231dc3bb 100644 --- a/locales/de.json +++ b/locales/de.json @@ -353,6 +353,14 @@ "placeholder": "Leer lassen, um aria2c aus dem PATH zu verwenden" }, "aria2HelpLink": "Erfahren Sie, wie Sie das aria2-Download-Backend einrichten", + "unknownBaseModelRouting": { + "label": "Routing unbekannter Basismodelle", + "help": "Legt fest, wohin Checkpoint-Downloads gehen, wenn CivitAI ein Basismodell meldet, das weder ein bekanntes Checkpoint (SD 1.x/2.x/3.x, SDXL, Pony, Illustrious, NoobAI) noch ein bekanntes Diffusionsmodell ist. Neue Diffusionsarchitekturen erscheinen häufig, daher ist das Routing zu Diffusionsmodellen meist korrekt.", + "options": { + "diffusionModel": "Diffusionsmodelle", + "checkpoint": "Checkpoints" + } + }, "civitaiHostBanner": { "title": "CivitAI-Host-Einstellung verfügbar", "content": "CivitAI verwendet jetzt civitai.com für SFW-Inhalte und civitai.red für uneingeschränkte Inhalte. In den Einstellungen können Sie ändern, welche Seite standardmäßig geöffnet wird.", @@ -2770,7 +2778,7 @@ "title": "Konflikte durch doppelte Dateinamen" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "Zentralisierte Sidecar-Dateien" }, "ui_version": { "title": "UI-Version" diff --git a/locales/en.json b/locales/en.json index 96da919b..e28de79d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -353,6 +353,14 @@ "placeholder": "Leave empty to use aria2c from PATH" }, "aria2HelpLink": "Learn how to set up the aria2 download backend", + "unknownBaseModelRouting": { + "label": "Unknown base model routing", + "help": "Decides where checkpoint downloads go when CivitAI reports a base model that is neither a known checkpoint (SD 1.x/2.x/3.x, SDXL, Pony, Illustrious, NoobAI) nor a known diffusion model. New diffusion architectures appear frequently, so routing them to diffusion models is usually correct.", + "options": { + "diffusionModel": "Diffusion models", + "checkpoint": "Checkpoints" + } + }, "civitaiHostBanner": { "title": "CivitAI host preference available", "content": "CivitAI now uses civitai.com for SFW content and civitai.red for unrestricted content. You can change which site opens by default in Settings.", diff --git a/locales/es.json b/locales/es.json index d301e746..a4af9c81 100644 --- a/locales/es.json +++ b/locales/es.json @@ -353,6 +353,14 @@ "placeholder": "Déjalo vacío para usar aria2c desde el PATH" }, "aria2HelpLink": "Aprende a configurar el backend de descarga aria2", + "unknownBaseModelRouting": { + "label": "Enrutamiento de modelos base desconocidos", + "help": "Decide a dónde van las descargas de Checkpoint cuando CivitAI informa de un modelo base que no es ni un Checkpoint conocido (SD 1.x/2.x/3.x, SDXL, Pony, Illustrious, NoobAI) ni un modelo de difusión conocido. Las nuevas arquitecturas de difusión aparecen con frecuencia, por lo que enrutarlas a modelos de difusión suele ser correcto.", + "options": { + "diffusionModel": "Modelos de difusión", + "checkpoint": "Checkpoints" + } + }, "civitaiHostBanner": { "title": "Preferencia de host de CivitAI disponible", "content": "CivitAI ahora usa civitai.com para contenido SFW y civitai.red para contenido sin restricciones. Puedes cambiar en Ajustes qué sitio se abre por defecto.", @@ -2770,7 +2778,7 @@ "title": "Conflictos de nombres de archivo duplicados" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "Archivos sidecar centralizados" }, "ui_version": { "title": "Versión de la interfaz" diff --git a/locales/fr.json b/locales/fr.json index 8ad8cc02..c87f7bbd 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -353,6 +353,14 @@ "placeholder": "Laisser vide pour utiliser aria2c depuis le PATH" }, "aria2HelpLink": "Apprenez à configurer le backend de téléchargement aria2", + "unknownBaseModelRouting": { + "label": "Routage des modèles de base inconnus", + "help": "Détermine où vont les téléchargements de Checkpoint lorsque CivitAI signale un modèle de base qui n’est ni un Checkpoint connu (SD 1.x/2.x/3.x, SDXL, Pony, Illustrious, NoobAI) ni un modèle de diffusion connu. De nouvelles architectures de diffusion apparaissent fréquemment, donc les router vers les modèles de diffusion est généralement correct.", + "options": { + "diffusionModel": "Modèles de diffusion", + "checkpoint": "Checkpoints" + } + }, "civitaiHostBanner": { "title": "Préférence d’hôte CivitAI disponible", "content": "CivitAI utilise désormais civitai.com pour le contenu SFW et civitai.red pour le contenu sans restriction. Vous pouvez modifier dans les paramètres le site ouvert par défaut.", @@ -2770,7 +2778,7 @@ "title": "Conflits de noms de fichiers en double" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "Fichiers sidecar centralisés" }, "ui_version": { "title": "Version de l'interface" diff --git a/locales/he.json b/locales/he.json index cfca1ccd..f6c9915e 100644 --- a/locales/he.json +++ b/locales/he.json @@ -353,6 +353,14 @@ "placeholder": "השאר ריק כדי להשתמש ב-aria2c מתוך ה-PATH" }, "aria2HelpLink": "למד כיצד להגדיר את מנוע ההורדה aria2", + "unknownBaseModelRouting": { + "label": "ניתוב מודל בסיס לא מוכר", + "help": "קובע לאן מועברות הורדות של Checkpoint כאשר CivitAI מדווח על מודל בסיס שהוא לא Checkpoint מוכר (SD 1.x/2.x/3.x, SDXL, Pony, Illustrious, NoobAI) ולא מודל דיפוזיה מוכר. ארכיטקטורות דיפוזיה חדשות מופיעות לעיתים קרובות, ולכן ניתוב שלהן למודלי דיפוזיה הוא בדרך כלל הנכון.", + "options": { + "diffusionModel": "מודלי דיפוזיה", + "checkpoint": "Checkpoints" + } + }, "civitaiHostBanner": { "title": "העדפת מארח CivitAI זמינה", "content": "CivitAI משתמש כעת ב-civitai.com עבור תוכן SFW וב-civitai.red עבור תוכן ללא הגבלות. ניתן לשנות בהגדרות איזה אתר ייפתח כברירת מחדל.", @@ -2770,7 +2778,7 @@ "title": "התנגשויות שמות קבצים כפולים" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "קבצי לוואי מרוכזים" }, "ui_version": { "title": "גרסת הממשק" diff --git a/locales/ja.json b/locales/ja.json index 14ebd64c..47372056 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -353,6 +353,14 @@ "placeholder": "空欄のままにすると PATH 上の aria2c を使用します" }, "aria2HelpLink": "aria2 ダウンロードバックエンドの設定方法", + "unknownBaseModelRouting": { + "label": "不明なベースモデルの振り分け", + "help": "CivitAIが報告するベースモデルが既知のCheckpoint(SD 1.x/2.x/3.x、SDXL、Pony、Illustrious、NoobAI)でも既知のDiffusion Modelでもない場合に、Checkpointのダウンロードの保存先を決定します。新しい拡散アーキテクチャは頻繁に登場するため、通常はDiffusion Modelへの振り分けが正しい選択です。", + "options": { + "diffusionModel": "Diffusion Model", + "checkpoint": "Checkpoint" + } + }, "civitaiHostBanner": { "title": "CivitAI ホスト設定を利用できます", "content": "CivitAI は現在、SFW コンテンツには civitai.com、制限なしコンテンツには civitai.red を使用しています。設定で既定で開くサイトを変更できます。", @@ -2770,7 +2778,7 @@ "title": "ファイル名重複競合" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "集中保存のサイドカーファイル" }, "ui_version": { "title": "UI バージョン" diff --git a/locales/ko.json b/locales/ko.json index c221ff42..dfe20317 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -353,6 +353,14 @@ "placeholder": "비워 두면 PATH의 aria2c를 사용합니다" }, "aria2HelpLink": "aria2 다운로드 백엔드 설정 방법 알아보기", + "unknownBaseModelRouting": { + "label": "알 수 없는 베이스 모델 라우팅", + "help": "CivitAI가 보고한 베이스 모델이 알려진 Checkpoint(SD 1.x/2.x/3.x, SDXL, Pony, Illustrious, NoobAI)도 알려진 Diffusion Model도 아닐 때 Checkpoint 다운로드의 저장 위치를 결정합니다. 새로운 확산 아키텍처는 자주 등장하므로 일반적으로 Diffusion Model로 라우팅하는 것이 올바릅니다.", + "options": { + "diffusionModel": "Diffusion Model", + "checkpoint": "Checkpoint" + } + }, "civitaiHostBanner": { "title": "CivitAI 호스트 기본 설정 사용 가능", "content": "이제 CivitAI는 SFW 콘텐츠에 civitai.com을, 무제한 콘텐츠에 civitai.red를 사용합니다. 설정에서 기본으로 열 사이트를 변경할 수 있습니다.", @@ -2770,7 +2778,7 @@ "title": "파일명 중복 충돌" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "중앙 집중식 사이드카 파일" }, "ui_version": { "title": "UI 버전" diff --git a/locales/ru.json b/locales/ru.json index 94b6cfce..1c92c656 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -353,6 +353,14 @@ "placeholder": "Оставьте пустым, чтобы использовать aria2c из PATH" }, "aria2HelpLink": "Узнайте, как настроить сервер загрузки aria2", + "unknownBaseModelRouting": { + "label": "Маршрутизация неизвестных базовых моделей", + "help": "Определяет, куда отправляются загрузки Checkpoint, когда CivitAI сообщает базовую модель, которая не является ни известным Checkpoint (SD 1.x/2.x/3.x, SDXL, Pony, Illustrious, NoobAI), ни известной диффузионной моделью. Новые диффузионные архитектуры появляются часто, поэтому маршрутизация их в диффузионные модели обычно правильная.", + "options": { + "diffusionModel": "Диффузионные модели", + "checkpoint": "Checkpoints" + } + }, "civitaiHostBanner": { "title": "Доступна настройка хоста CivitAI", "content": "Теперь CivitAI использует civitai.com для контента SFW и civitai.red для контента без ограничений. В настройках можно изменить, какой сайт открывать по умолчанию.", @@ -2770,7 +2778,7 @@ "title": "Конфликты дублирующихся имён файлов" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "Централизованные sidecar-файлы" }, "ui_version": { "title": "Версия интерфейса" diff --git a/locales/zh-CN.json b/locales/zh-CN.json index df3fd027..9a5013b4 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -353,6 +353,14 @@ "placeholder": "留空则使用 PATH 中的 aria2c" }, "aria2HelpLink": "了解如何配置 aria2 下载后端", + "unknownBaseModelRouting": { + "label": "未知基础模型路由", + "help": "当 CivitAI 报告的基础模型既不是已知的 Checkpoint(SD 1.x/2.x/3.x、SDXL、Pony、Illustrious、NoobAI),也不是已知的扩散模型时,决定 Checkpoint 下载的保存位置。新的扩散架构频繁出现,因此将其路由到扩散模型通常是正确的。", + "options": { + "diffusionModel": "扩散模型", + "checkpoint": "Checkpoint" + } + }, "civitaiHostBanner": { "title": "已提供 CivitAI 站点偏好设置", "content": "CivitAI 现在使用 civitai.com 提供 SFW 内容,使用 civitai.red 提供无限制内容。你可以在设置中更改默认打开的站点。", @@ -2770,7 +2778,7 @@ "title": "文件名重复冲突" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "集中存储的附属文件" }, "ui_version": { "title": "UI 版本" diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 589e3c9a..f0d5f544 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -353,6 +353,14 @@ "placeholder": "留空則使用 PATH 中的 aria2c" }, "aria2HelpLink": "了解如何設定 aria2 下載後端", + "unknownBaseModelRouting": { + "label": "未知基礎模型路由", + "help": "當 CivitAI 回報的基礎模型既不是已知的 Checkpoint(SD 1.x/2.x/3.x、SDXL、Pony、Illustrious、NoobAI),也不是已知的擴散模型時,決定 Checkpoint 下載的儲存位置。新的擴散架構頻繁出現,因此將其路由到擴散模型通常是正確的。", + "options": { + "diffusionModel": "擴散模型", + "checkpoint": "Checkpoint" + } + }, "civitaiHostBanner": { "title": "已提供 CivitAI 站點偏好設定", "content": "CivitAI 現在使用 civitai.com 提供 SFW 內容,使用 civitai.red 提供無限制內容。您可以在設定中變更預設開啟的站點。", @@ -2770,7 +2778,7 @@ "title": "檔案名稱重複衝突" }, "sidecar_mirror_orphans": { - "title": "[TODO: Translate] Centralized Sidecars" + "title": "集中儲存的附屬檔案" }, "ui_version": { "title": "UI 版本" diff --git a/py/routes/handlers/download_routing_handlers.py b/py/routes/handlers/download_routing_handlers.py index 20ecb7ef..8107afc5 100644 --- a/py/routes/handlers/download_routing_handlers.py +++ b/py/routes/handlers/download_routing_handlers.py @@ -55,6 +55,11 @@ class DownloadRoutingHandler: status=400, ) + # CivitAI ModelType.UNet downloads go through the checkpoint branch, + # same as in the download manager. + if model_type.lower() == "unet": + model_type = "checkpoint" + if model_type.lower() in VALID_OTHER_CIVITAI_TYPES: from ...services.settings_manager import get_settings_manager @@ -96,10 +101,15 @@ class DownloadRoutingHandler: } ) + from ...services.settings_manager import get_settings_manager + is_diffusion = is_diffusion_model_download( model_type, file_types=(str(t) for t in file_types), base_model=base_model, + unknown_base_model_default=get_settings_manager().get( + "unknown_base_model_routing", "diffusion_model" + ), ) return web.json_response( { diff --git a/py/services/download_manager.py b/py/services/download_manager.py index b2cf40ec..62fff77a 100644 --- a/py/services/download_manager.py +++ b/py/services/download_manager.py @@ -1523,7 +1523,11 @@ class DownloadManager: return {"success": False, "error": "Failed to fetch model metadata"} model_type_from_info = version_info.get("model", {}).get("type", "").lower() - if model_type_from_info == "checkpoint": + # CivitAI ModelType has no model-level diffusion variant: DiT + # models are uploaded as "Checkpoint" or "UNet". Both go through + # the checkpoint branch so the standard diffusion routing chain + # (file type -> baseModel lists -> unknown default) applies. + if model_type_from_info in ("checkpoint", "unet"): model_type = "checkpoint" elif model_type_from_info in VALID_LORA_TYPES: model_type = "lora" @@ -1670,6 +1674,9 @@ class DownloadManager: model_type, file_types=(f.get("type", "") for f in version_info.get("files", [])), base_model=base_model_value, + unknown_base_model_default=get_settings_manager().get( + "unknown_base_model_routing", "diffusion_model" + ), ) # Existence check after the metadata fetch (#1058): diff --git a/py/services/download_routing.py b/py/services/download_routing.py index a18c2080..175ca9a4 100644 --- a/py/services/download_routing.py +++ b/py/services/download_routing.py @@ -13,6 +13,7 @@ import logging from typing import Iterable, Optional from ..utils.constants import ( + CHECKPOINT_BASE_MODELS, CIVITAI_FILE_TYPE_TO_OTHER_SUB_TYPE, CIVITAI_TYPE_TO_OTHER_SUB_TYPE, DIFFUSION_MODEL_BASE_MODELS, @@ -24,17 +25,27 @@ logger = logging.getLogger(__name__) # model (loaded via UNETLoader in ComfyUI) rather than a full checkpoint. DIFFUSION_FILE_TYPES = frozenset({"UNet", "Diffusion Model"}) +# Allowed values for the "unknown_base_model_routing" setting / the +# unknown_base_model_default parameter below. +ROUTING_DIFFUSION_MODEL = "diffusion_model" +ROUTING_CHECKPOINT = "checkpoint" + def is_diffusion_model_download( model_type: str, file_types: Iterable[str] = (), base_model: str = "", + unknown_base_model_default: str = ROUTING_DIFFUSION_MODEL, ) -> bool: """Return True when a download should be routed to the unet roots. Only applies to downloads initiated from the checkpoint library. Priority: (1) any file has type "UNet" or "Diffusion Model" (the more - direct signal from CivitAI), (2) baseModel is a known diffusion model. + direct signal from CivitAI), (2) baseModel is a known diffusion model, + (3) baseModel is a known full checkpoint -> not diffusion, (4) unknown + or empty baseModel -> the ``unknown_base_model_default`` setting, which + defaults to diffusion because the set of true checkpoint families is + closed while new DiT base models appear all the time. """ if model_type != "checkpoint": return False @@ -54,7 +65,16 @@ def is_diffusion_model_download( ) return True - return False + if base_model in CHECKPOINT_BASE_MODELS: + return False + + is_diffusion = unknown_base_model_default != ROUTING_CHECKPOINT + logger.info( + "baseModel '%s' is unknown, routing to %s folder (unknown_base_model_routing)", + base_model, + "unet" if is_diffusion else "checkpoint", + ) + return is_diffusion def resolve_other_download_sub_type( diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 0e874776..df564d30 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -130,6 +130,9 @@ DEFAULT_SETTINGS: Dict[str, Any] = { "metadata_refresh_skip_paths": [], "skip_previously_downloaded_model_versions": False, "download_skip_base_models": [], + # Routing target for checkpoint downloads whose baseModel is neither a + # known diffusion model nor a known full checkpoint (CHECKPOINT_BASE_MODELS). + "unknown_base_model_routing": "diffusion_model", "backup_auto_enabled": True, "backup_retention_count": 5, "use_new_license_icons": True, @@ -1628,6 +1631,16 @@ class SettingsManager: return normalized return "alongside" + @staticmethod + def _normalize_unknown_base_model_routing(value: Any) -> str: + """Return a valid unknown-base-model routing target, falling back to ``diffusion_model``.""" + + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in ("diffusion_model", "checkpoint"): + return normalized + return "diffusion_model" + def _refresh_sidecar_storage_config(self) -> None: """Rebuild dependent config state after sidecar storage settings change.""" @@ -1840,6 +1853,8 @@ class SettingsManager: self._migrate_recipes_directory(current_recipes_dir, target_recipes_dir) elif key == "sidecar_storage_mode": value = self._normalize_sidecar_storage_mode(value) + elif key == "unknown_base_model_routing": + value = self._normalize_unknown_base_model_routing(value) elif key == "sidecar_storage_path": value = self._normalize_recipes_path_value(value) self.settings[key] = value diff --git a/py/utils/constants.py b/py/utils/constants.py index 6a283ced..9a9341ca 100644 --- a/py/utils/constants.py +++ b/py/utils/constants.py @@ -352,12 +352,15 @@ DIFFUSION_MODEL_BASE_MODELS = frozenset( "PixArt E", # Video diffusion models "CogVideoX", + "Flux 3 Video", "Hunyuan Video", "LTXV", "LTXV2", "LTXV 2.3", + "LTXV 2.5", "Mochi", "SVD", + "SVD XT", "Wan Video", "Wan Video 1.3B t2v", "Wan Video 14B t2v", @@ -368,11 +371,19 @@ DIFFUSION_MODEL_BASE_MODELS = frozenset( "Wan Video 2.2 T2V-A14B", "Wan Video 2.5 T2V", "Wan Video 2.5 I2V", + "Wan Video 2.7", + "Wan Video 3.0", # Other diffusion models + "Boogu", "Ernie", "Ernie Turbo", + "HiDream-O1", + "Ming Image Design 0.1", + "Ming Image Design Layer 0.1", + "MiniMax H3", "Nucleus", "Qwen", + "Wan Image 2.7", "ZImageBase", "ZImageTurbo", # Krea 2 — loaded via UNETLoader in ComfyUI @@ -380,6 +391,53 @@ DIFFUSION_MODEL_BASE_MODELS = frozenset( ] ) +# baseModel values from CivitAI that are true full checkpoints (loaded via +# CheckpointLoaderSimple in ComfyUI). New DiT families appear on CivitAI all +# the time, so download routing inverts the fallback: anything NOT in this +# closed set (and not a known diffusion model) is treated as a diffusion +# model by default (see py/services/download_routing.py). Cross-checked +# against CivitAI's official baseModelRecords (packages/civitai-shared +# src/basemodel.constants.ts), not only the download skip list. +# "Pony V7" is deliberately excluded: verified via the live API (model +# 1901521) to be AuraFlow-architecture shipping .gguf variants (UNETLoader), +# so it follows the unknown-base-model default (diffusion). +CHECKPOINT_BASE_MODELS = frozenset( + [ + # Stable Diffusion 1.x + "SD 1.4", + "SD 1.5", + "SD 1.5 LCM", + "SD 1.5 Hyper", + # Stable Diffusion 2.x + "SD 2.0", + "SD 2.0 768", + "SD 2.1", + "SD 2.1 768", + "SD 2.1 Unclip", + # Stable Diffusion 3.x + "SD 3", + "SD 3.5", + "SD 3.5 Medium", + "SD 3.5 Large", + "SD 3.5 Large Turbo", + # SDXL and its full-checkpoint derivatives + "SDXL 0.9", + "SDXL 1.0", + "SDXL 1.0 LCM", + "SDXL Lightning", + "SDXL Hyper", + "SDXL Turbo", + "SDXL Distilled", + "Pony", + "Illustrious", + "NoobAI", + # Other full-checkpoint families + "Playground v2", + # Stable Cascade is unCLIP-style but loads via CheckpointLoader + "Stable Cascade", + ] +) + # Supported baseModel values for download exclusion settings. # Keep this aligned with static/js/utils/constants.js, excluding the generic "Other" value. SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS = frozenset( diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 808e4692..e6763d97 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -1053,6 +1053,11 @@ export class SettingsManager { downloadBackendSelect.value = state.global.settings.download_backend || 'python'; } + const unknownBaseModelRoutingSelect = document.getElementById('unknownBaseModelRouting'); + if (unknownBaseModelRoutingSelect) { + unknownBaseModelRoutingSelect.value = state.global.settings.unknown_base_model_routing || 'diffusion_model'; + } + const aria2cPathInput = document.getElementById('aria2cPath'); if (aria2cPathInput) { aria2cPathInput.value = state.global.settings.aria2c_path || ''; diff --git a/static/js/state/index.js b/static/js/state/index.js index f50e5ac8..acf76dd3 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -60,6 +60,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({ metadata_refresh_skip_paths: [], skip_previously_downloaded_model_versions: false, download_skip_base_models: [], + unknown_base_model_routing: 'diffusion_model', backup_auto_enabled: true, backup_retention_count: 5, sidecar_storage_mode: 'alongside', diff --git a/templates/components/modals/settings/library.html b/templates/components/modals/settings/library.html index 78a318ed..2b50fa3d 100644 --- a/templates/components/modals/settings/library.html +++ b/templates/components/modals/settings/library.html @@ -26,6 +26,11 @@ {{ sm.setting_select('defaultUnetRoot', 'default_unet_root', 'settings.folderSettings.defaultUnetRoot', [], 'settings.folderSettings.defaultUnetRootHelp') }} + {{ sm.setting_select('unknownBaseModelRouting', 'unknown_base_model_routing', 'settings.unknownBaseModelRouting.label', [ + ('diffusion_model', 'settings.unknownBaseModelRouting.options.diffusionModel'), + ('checkpoint', 'settings.unknownBaseModelRouting.options.checkpoint'), + ], 'settings.unknownBaseModelRouting.help') }} + {{ sm.setting_select('defaultEmbeddingRoot', 'default_embedding_root', 'settings.folderSettings.defaultEmbeddingRoot', [], 'settings.folderSettings.defaultEmbeddingRootHelp') }} {{ sm.setting_toggle('enableOtherModels', 'enable_other_models', 'settings.folderSettings.enableOtherModels', 'settings.folderSettings.enableOtherModelsHelp') }} diff --git a/tests/routes/test_download_routing_handlers.py b/tests/routes/test_download_routing_handlers.py index 4e1474b6..05695dfa 100644 --- a/tests/routes/test_download_routing_handlers.py +++ b/tests/routes/test_download_routing_handlers.py @@ -73,6 +73,57 @@ async def test_regular_checkpoint_stays_on_checkpoint_root(): assert payload["root_kind"] == "checkpoint" +@pytest.mark.asyncio +async def test_unknown_base_model_routes_to_unet_by_default(): + handler = DownloadRoutingHandler() + response = await handler.get_download_routing( + FakeRequest( + { + "model_type": "checkpoint", + "base_model": "Brand New Arch", + "file_types": ["Model"], + } + ) + ) + payload = json.loads(response.text) + assert payload["is_diffusion_model"] is True + assert payload["root_kind"] == "unet" + + +@pytest.mark.asyncio +async def test_unknown_base_model_honors_checkpoint_routing_setting(): + get_settings_manager().settings["unknown_base_model_routing"] = "checkpoint" + + handler = DownloadRoutingHandler() + response = await handler.get_download_routing( + FakeRequest( + { + "model_type": "checkpoint", + "base_model": "Brand New Arch", + "file_types": ["Model"], + } + ) + ) + payload = json.loads(response.text) + assert payload["is_diffusion_model"] is False + assert payload["root_kind"] == "checkpoint" + + +@pytest.mark.asyncio +async def test_unet_model_type_uses_checkpoint_branch(): + """CivitAI ModelType.UNet has no branch of its own: it goes through the + checkpoint branch, where its UNet-typed files route to the unet roots.""" + handler = DownloadRoutingHandler() + response = await handler.get_download_routing( + FakeRequest( + {"model_type": "unet", "base_model": "MiniMax H3", "file_types": ["UNet"]} + ) + ) + payload = json.loads(response.text) + assert payload["is_diffusion_model"] is True + assert payload["root_kind"] == "unet" + + @pytest.mark.asyncio async def test_lora_is_never_diffusion(): handler = DownloadRoutingHandler() diff --git a/tests/services/test_download_manager_basic.py b/tests/services/test_download_manager_basic.py index 97fc974b..39066cb6 100644 --- a/tests/services/test_download_manager_basic.py +++ b/tests/services/test_download_manager_basic.py @@ -2200,6 +2200,54 @@ async def test_download_uses_raw_file_name_from_mini_endpoint( ] +@pytest.mark.asyncio +async def test_unet_model_type_download_lands_in_unet_roots( + monkeypatch, scanners, metadata_provider, tmp_path +): + """CivitAI ModelType.UNet goes through the checkpoint branch: its + UNet-typed files route to the unet roots like any diffusion model.""" + manager = DownloadManager() + unet_root = tmp_path / "unet" + get_settings_manager().settings["default_unet_root"] = str(unet_root) + metadata_provider.payload = { + "id": 42, + "model": {"type": "UNet", "tags": ["fantasy"]}, + "baseModel": "MiniMax H3", + "creator": {"username": "Author"}, + "files": [ + { + "id": 1001, + "type": "UNet", + "primary": True, + "name": "minimax_h3.safetensors", + "downloadUrl": "https://example.invalid/file.safetensors", + } + ], + } + + captured = {} + + async def fake_execute_download(self, **kwargs): + captured["file_path"] = kwargs["metadata"].file_path + return {"success": True} + + monkeypatch.setattr( + DownloadManager, "_execute_download", fake_execute_download, raising=False + ) + + result = await manager.download_from_civitai( + model_version_id=42, + save_dir=str(tmp_path), + use_default_paths=True, + progress_callback=None, + source=None, + ) + + assert result["success"] is True, result + assert captured["file_path"].startswith(str(unet_root)) + assert captured["file_path"].endswith("minimax_h3.safetensors") + + @pytest.mark.asyncio async def test_download_falls_back_to_rest_name_when_mini_fails( monkeypatch, scanners, metadata_provider, tmp_path @@ -2209,7 +2257,7 @@ async def test_download_falls_back_to_rest_name_when_mini_fails( metadata_provider.payload = { "id": 42, "model": {"type": "Checkpoint", "tags": ["fantasy"]}, - "baseModel": "BaseModel", + "baseModel": "SDXL 1.0", "creator": {"username": "Author"}, "files": [ { diff --git a/tests/services/test_download_routing.py b/tests/services/test_download_routing.py index 07d78bd1..b118b0a6 100644 --- a/tests/services/test_download_routing.py +++ b/tests/services/test_download_routing.py @@ -20,9 +20,68 @@ def test_base_model_fallback_routes_to_unet(): ) -def test_regular_checkpoint_stays_on_checkpoint_roots(): +@pytest.mark.parametrize("base_model", ["SVD XT", "Boogu", "Wan Video 3.0", "Flux 3 Video"]) +def test_new_diffusion_families_route_to_unet(base_model): + """DiT families from CivitAI's official baseModel list that plain + "Model"-typed files must not strand on the checkpoint roots.""" + assert is_diffusion_model_download( + "checkpoint", file_types=["Model"], base_model=base_model + ) + + +def test_minimax_h3_routes_to_unet(): + """CivitAI model 2877206: type "Checkpoint", baseModel "MiniMax H3", + all file entries typed "Model".""" + assert is_diffusion_model_download( + "checkpoint", file_types=["Model"], base_model="MiniMax H3" + ) + + +@pytest.mark.parametrize( + "base_model", + [ + "SDXL 1.0", + "Illustrious", + "SD 1.5", + "SD 2.1 768", + "SD 3.5 Large", + "SDXL Turbo", + "Playground v2", + "Stable Cascade", + ], +) +def test_known_checkpoint_base_models_stay_on_checkpoint_roots(base_model): + """CHECKPOINT_BASE_MODELS members never route to unet, even when the + unknown-base-model default is diffusion.""" assert not is_diffusion_model_download( - "checkpoint", file_types=["Model"], base_model="SDXL 1.0" + "checkpoint", file_types=["Model"], base_model=base_model + ) + + +def test_unknown_base_model_defaults_to_unet(): + """New DiT families appear faster than the allowlist can track them, so + unknown baseModels route to unet by default.""" + assert is_diffusion_model_download( + "checkpoint", file_types=["Model"], base_model="Brand New Arch" + ) + + +def test_unknown_base_model_honors_checkpoint_setting(): + assert not is_diffusion_model_download( + "checkpoint", + file_types=["Model"], + base_model="Brand New Arch", + unknown_base_model_default="checkpoint", + ) + + +def test_unknown_base_model_checkpoint_setting_still_loses_to_file_type(): + """The file-type signal stays first regardless of the setting.""" + assert is_diffusion_model_download( + "checkpoint", + file_types=["Diffusion Model"], + base_model="SDXL 1.0", + unknown_base_model_default="checkpoint", ) @@ -35,9 +94,15 @@ def test_non_checkpoint_types_never_route_to_unet(): ) -def test_empty_inputs_stay_on_checkpoint_roots(): - assert not is_diffusion_model_download("checkpoint") - assert not is_diffusion_model_download("checkpoint", file_types=[], base_model="") +def test_empty_base_model_follows_unknown_default(): + assert is_diffusion_model_download("checkpoint") + assert is_diffusion_model_download("checkpoint", file_types=[], base_model="") + assert not is_diffusion_model_download( + "checkpoint", + file_types=[], + base_model="", + unknown_base_model_default="checkpoint", + ) from py.services.download_routing import resolve_other_download_sub_type diff --git a/tests/services/test_settings_manager.py b/tests/services/test_settings_manager.py index f06391ae..4bdbd44d 100644 --- a/tests/services/test_settings_manager.py +++ b/tests/services/test_settings_manager.py @@ -1300,6 +1300,21 @@ def test_setting_download_skip_base_models_normalizes_string_input(manager): assert manager.get("download_skip_base_models") == ["SDXL 1.0", "Pony"] +def test_unknown_base_model_routing_defaults_to_diffusion_model(manager): + assert manager.get("unknown_base_model_routing") == "diffusion_model" + + +def test_unknown_base_model_routing_is_normalized(manager): + manager.set("unknown_base_model_routing", "CHECKPOINT") + assert manager.get("unknown_base_model_routing") == "checkpoint" + + manager.set("unknown_base_model_routing", "bogus") + assert manager.get("unknown_base_model_routing") == "diffusion_model" + + manager.set("unknown_base_model_routing", None) + assert manager.get("unknown_base_model_routing") == "diffusion_model" + + def test_skip_previously_downloaded_model_versions_defaults_false(manager): assert manager.get_skip_previously_downloaded_model_versions() is False