mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(other-models): announce the feature only when folders are available
Other Models management is opt-in and its folders come from folder_paths.get_folder_paths(). In plugin mode ComfyUI registers vae, upscale_models, text_encoders, clip_vision and controlnet out of the box, so enabling the feature works immediately. Standalone only knows the keys present in settings.json.folder_paths, and that file is edited by hand - there is no UI for those keys - so a standalone user who followed the announcement banner reached "Enable Other Models" and then an empty page. Gate the announcement on the capability instead of on how the process was started: - Config.get_other_models_availability() probes every canonical other key (legacy clip collapses into text_encoders where the host exposes map_legacy) and reports which sub_types resolve to a folder that exists on disk. It deliberately ignores enable_other_models: the question is "could this work here at all?". An empty folder counts, because CivitAI downloads can target it. - /api/lm/settings exposes it as the derived, non-persisted other_models_paths_available flag; a probe failure yields null and the banner fails open. - BannerService only registers the announcement when the flag is not false. `=== false` (not falsy) keeps a cached/older payload working, and nothing is written to dismissed_banners, so the banner can return once folders exist. - The Other page grows an "enabled but nothing to scan" empty state driven by config.other_roots, showing the settings.json snippet for standalone and a pointer to ComfyUI model paths otherwise, plus an Open Settings action. It also covers the corner where only a non-default sub_type has a folder. Translate the six other.noPaths.* keys into all nine locales and record the new "folder key" / "on disk" terminology in the i18n guidelines. Backend tests and pytest tests/i18n could not run in this environment (no pytest/platformdirs); the probe was exercised against a stubbed folder_paths. Frontend: 120 files / 1101 JS tests passed.
This commit is contained in:
@@ -27,6 +27,12 @@ Locales: `en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `ru`, `he` (RTL).
|
|||||||
> default set — keep all three enumerating the full five (`VAE / upscaler / text encoder /
|
> default set — keep all three enumerating the full five (`VAE / upscaler / text encoder /
|
||||||
> CLIP vision / ControlNet` in `en`; locale slash-list casing follows each file's existing
|
> CLIP vision / ControlNet` in `en`; locale slash-list casing follows each file's existing
|
||||||
> `VAE / Upscaler / Text Encoder / …` style, de compounds as `CLIP-Vision- und ControlNet-Ordner`).
|
> `VAE / Upscaler / Text Encoder / …` style, de compounds as `CLIP-Vision- und ControlNet-Ordner`).
|
||||||
|
>
|
||||||
|
> **Status (2026-09, "no folders found" state):** the Other Models page gained an *enabled but
|
||||||
|
> nothing to scan* empty state with 6 new keys (`other.noPaths.*`); translated in all 9 locales
|
||||||
|
> in the same pass. The `folder_paths` JSON snippet shown in that state lives in
|
||||||
|
> `templates/other.html`, **not** in the locale files, so it is never translated — only the
|
||||||
|
> surrounding prose is. Terminology added in §2.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -273,6 +279,19 @@ In prose these names sit next to localized nouns the same way `Diffusion Model`
|
|||||||
each locale's `header.filter.modelTypes` rendering (zh `管理的模型类型`, ja `管理するモデルタイプ`,
|
each locale's `header.filter.modelTypes` rendering (zh `管理的模型类型`, ja `管理するモデルタイプ`,
|
||||||
de `Verwaltete Modelltypen`, …).
|
de `Verwaltete Modelltypen`, …).
|
||||||
|
|
||||||
|
The "no folders found" empty state (`other.noPaths.*`) uses two phrases that must stay
|
||||||
|
consistent whenever that copy is edited. `folder key` means the `folder_paths` key name
|
||||||
|
(`vae`, `upscale_models`, … — Latin per the table above); `on disk` means the folder must
|
||||||
|
physically exist:
|
||||||
|
|
||||||
|
| Phrase | Rendering |
|
||||||
|
|---|---|
|
||||||
|
| folder key | zh-CN 文件夹键 · zh-TW 資料夾鍵 · ja フォルダーキー · ko 폴더 키 · fr clé de dossier · de Ordnerschlüssel · es clave de carpeta · ru ключ папки · he מפתח תיקייה |
|
||||||
|
| on disk | zh-CN 在磁盘上 · zh-TW 在磁碟上 · ja ディスク上 · ko 디스크에 · fr sur le disque · de auf dem Datenträger · es en el disco · ru на диске · he בדיסק |
|
||||||
|
|
||||||
|
`settings.json` and `ComfyUI` stay verbatim in every locale; "reload this page" / "restart
|
||||||
|
LoRA Manager" reuse each locale's existing restart wording (`settings.extraFolderPaths.*`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Cross-cutting confusion hot-spots (must-fix list)
|
## 3. Cross-cutting confusion hot-spots (must-fix list)
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "Aktivierung weiterer Modelle fehlgeschlagen",
|
"enableFailed": "Aktivierung weiterer Modelle fehlgeschlagen",
|
||||||
"downloadBlocked": "Die Verwaltung weiterer Modelle ist für diesen Modelltyp deaktiviert. Aktivieren Sie sie unter Einstellungen > Bibliothek, um diese Datei herunterzuladen.",
|
"downloadBlocked": "Die Verwaltung weiterer Modelle ist für diesen Modelltyp deaktiviert. Aktivieren Sie sie unter Einstellungen > Bibliothek, um diese Datei herunterzuladen.",
|
||||||
"enableAction": "Weitere Modelle aktivieren"
|
"enableAction": "Weitere Modelle aktivieren"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "Keine Ordner für weitere Modelle gefunden",
|
||||||
|
"descriptionStandalone": "Die Verwaltung weiterer Modelle ist aktiviert, aber keiner der konfigurierten Modellordner existiert auf dem Datenträger. Fügen Sie die unten stehenden Ordnerpfade zu settings.json hinzu und starten Sie LoRA Manager neu.",
|
||||||
|
"hintStandalone": "Nur die oben aufgeführten Ordnerschlüssel werden gescannt; nicht benötigte Schlüssel können weggelassen werden.",
|
||||||
|
"descriptionComfyUI": "Die Verwaltung weiterer Modelle ist aktiviert, aber keiner der konfigurierten Modellordner existiert auf dem Datenträger. Fügen Sie die entsprechenden Modellordner zu Ihren ComfyUI-Modellpfaden hinzu und laden Sie diese Seite neu.",
|
||||||
|
"hintComfyUI": "Weitere Modelle werden aus den Ordnern vae, upscale_models, text_encoders, clip_vision und controlnet von ComfyUI gelesen.",
|
||||||
|
"openSettings": "Einstellungen öffnen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "Failed to enable Other Models",
|
"enableFailed": "Failed to enable Other Models",
|
||||||
"downloadBlocked": "Other Models management is disabled for this model type. Enable it in Settings > Library to download this file.",
|
"downloadBlocked": "Other Models management is disabled for this model type. Enable it in Settings > Library to download this file.",
|
||||||
"enableAction": "Enable Other Models"
|
"enableAction": "Enable Other Models"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "No other-model folders found",
|
||||||
|
"descriptionStandalone": "Other Models management is on, but none of the configured model folders exist on disk. Add the folder paths below to settings.json and restart LoRA Manager.",
|
||||||
|
"hintStandalone": "Only the folder keys listed above are scanned; keys you do not need can be omitted.",
|
||||||
|
"descriptionComfyUI": "Other Models management is on, but none of the configured model folders exist on disk. Add the matching model folders to your ComfyUI model paths, then reload this page.",
|
||||||
|
"hintComfyUI": "Other models are read from ComfyUI's vae, upscale_models, text_encoders, clip_vision and controlnet folders.",
|
||||||
|
"openSettings": "Open Settings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "No se pudieron activar los otros modelos",
|
"enableFailed": "No se pudieron activar los otros modelos",
|
||||||
"downloadBlocked": "La gestión de otros modelos está desactivada para este tipo de modelo. Actívala en Configuración > Biblioteca para descargar este archivo.",
|
"downloadBlocked": "La gestión de otros modelos está desactivada para este tipo de modelo. Actívala en Configuración > Biblioteca para descargar este archivo.",
|
||||||
"enableAction": "Activar otros modelos"
|
"enableAction": "Activar otros modelos"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "No se encontraron carpetas de otros modelos",
|
||||||
|
"descriptionStandalone": "La gestión de otros modelos está activada, pero ninguna de las carpetas de modelos configuradas existe en el disco. Añade las rutas de carpetas de abajo a settings.json y reinicia LoRA Manager.",
|
||||||
|
"hintStandalone": "Solo se escanean las claves de carpeta listadas arriba; las claves que no necesites puedes omitirlas.",
|
||||||
|
"descriptionComfyUI": "La gestión de otros modelos está activada, pero ninguna de las carpetas de modelos configuradas existe en el disco. Añade las carpetas de modelos correspondientes a tus rutas de modelos de ComfyUI y recarga esta página.",
|
||||||
|
"hintComfyUI": "Los otros modelos se leen de las carpetas vae, upscale_models, text_encoders, clip_vision y controlnet de ComfyUI.",
|
||||||
|
"openSettings": "Abrir configuración"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "Échec de l’activation des autres modèles",
|
"enableFailed": "Échec de l’activation des autres modèles",
|
||||||
"downloadBlocked": "La gestion des autres modèles est désactivée pour ce type de modèle. Activez-la dans Paramètres > Bibliothèque pour télécharger ce fichier.",
|
"downloadBlocked": "La gestion des autres modèles est désactivée pour ce type de modèle. Activez-la dans Paramètres > Bibliothèque pour télécharger ce fichier.",
|
||||||
"enableAction": "Activer les autres modèles"
|
"enableAction": "Activer les autres modèles"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "Aucun dossier d’autres modèles trouvé",
|
||||||
|
"descriptionStandalone": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés n’existe sur le disque. Ajoutez les chemins de dossiers ci-dessous à settings.json, puis redémarrez LoRA Manager.",
|
||||||
|
"hintStandalone": "Seules les clés de dossiers listées ci-dessus sont analysées ; les clés inutiles peuvent être omises.",
|
||||||
|
"descriptionComfyUI": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés n’existe sur le disque. Ajoutez les dossiers de modèles correspondants à vos chemins de modèles ComfyUI, puis rechargez cette page.",
|
||||||
|
"hintComfyUI": "Les autres modèles sont lus depuis les dossiers vae, upscale_models, text_encoders, clip_vision et controlnet de ComfyUI.",
|
||||||
|
"openSettings": "Ouvrir les paramètres"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "הפעלת המודלים האחרים נכשלה",
|
"enableFailed": "הפעלת המודלים האחרים נכשלה",
|
||||||
"downloadBlocked": "ניהול המודלים האחרים מושבת עבור סוג מודל זה. הפעל אותו בהגדרות > ספרייה כדי להוריד קובץ זה.",
|
"downloadBlocked": "ניהול המודלים האחרים מושבת עבור סוג מודל זה. הפעל אותו בהגדרות > ספרייה כדי להוריד קובץ זה.",
|
||||||
"enableAction": "הפעל מודלים אחרים"
|
"enableAction": "הפעל מודלים אחרים"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "לא נמצאו תיקיות של מודלים אחרים",
|
||||||
|
"descriptionStandalone": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את נתיבי התיקיות שלמטה ל-settings.json והפעל מחדש את LoRA Manager.",
|
||||||
|
"hintStandalone": "רק מפתחות התיקיות המפורטים למעלה נסרקים; ניתן להשמיט מפתחות שאינך צריך.",
|
||||||
|
"descriptionComfyUI": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את תיקיות המודלים המתאימות לנתיבי המודלים של ComfyUI וטען מחדש עמוד זה.",
|
||||||
|
"hintComfyUI": "מודלים אחרים נקראים מתיקיות vae, upscale_models, text_encoders, clip_vision ו-controlnet של ComfyUI.",
|
||||||
|
"openSettings": "פתח הגדרות"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "その他のモデルの有効化に失敗しました",
|
"enableFailed": "その他のモデルの有効化に失敗しました",
|
||||||
"downloadBlocked": "このモデルタイプではその他のモデル管理が無効です。このファイルをダウンロードするには「設定 > ライブラリ」で有効にしてください。",
|
"downloadBlocked": "このモデルタイプではその他のモデル管理が無効です。このファイルをダウンロードするには「設定 > ライブラリ」で有効にしてください。",
|
||||||
"enableAction": "その他のモデルを有効にする"
|
"enableAction": "その他のモデルを有効にする"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "その他のモデルのフォルダーが見つかりません",
|
||||||
|
"descriptionStandalone": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。以下のフォルダーパスをsettings.jsonに追加し、LoRA Managerを再起動してください。",
|
||||||
|
"hintStandalone": "スキャンされるのは上記のフォルダーキーのみです。不要なキーは省略できます。",
|
||||||
|
"descriptionComfyUI": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。該当するモデルフォルダーをComfyUIのモデルパスに追加し、このページを再読み込みしてください。",
|
||||||
|
"hintComfyUI": "その他のモデルは、ComfyUIのvae、upscale_models、text_encoders、clip_vision、controlnetフォルダーから読み込まれます。",
|
||||||
|
"openSettings": "設定を開く"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "기타 모델 활성화 실패",
|
"enableFailed": "기타 모델 활성화 실패",
|
||||||
"downloadBlocked": "이 모델 유형에 대해서는 기타 모델 관리가 비활성화되어 있습니다. 이 파일을 다운로드하려면 설정 > 라이브러리에서 활성화하세요.",
|
"downloadBlocked": "이 모델 유형에 대해서는 기타 모델 관리가 비활성화되어 있습니다. 이 파일을 다운로드하려면 설정 > 라이브러리에서 활성화하세요.",
|
||||||
"enableAction": "기타 모델 활성화"
|
"enableAction": "기타 모델 활성화"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "기타 모델 폴더를 찾을 수 없습니다",
|
||||||
|
"descriptionStandalone": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 아래 폴더 경로를 settings.json에 추가한 뒤 LoRA Manager를 재시작하세요.",
|
||||||
|
"hintStandalone": "위에 나열된 폴더 키만 스캔됩니다. 필요 없는 키는 생략할 수 있습니다.",
|
||||||
|
"descriptionComfyUI": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 해당 모델 폴더를 ComfyUI 모델 경로에 추가한 뒤 이 페이지를 새로 고침하세요.",
|
||||||
|
"hintComfyUI": "기타 모델은 ComfyUI의 vae, upscale_models, text_encoders, clip_vision, controlnet 폴더에서 읽어옵니다.",
|
||||||
|
"openSettings": "설정 열기"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "Не удалось включить другие модели",
|
"enableFailed": "Не удалось включить другие модели",
|
||||||
"downloadBlocked": "Управление другими моделями отключено для этого типа моделей. Включите его в разделе «Настройки > Библиотека», чтобы загрузить этот файл.",
|
"downloadBlocked": "Управление другими моделями отключено для этого типа моделей. Включите его в разделе «Настройки > Библиотека», чтобы загрузить этот файл.",
|
||||||
"enableAction": "Включить другие модели"
|
"enableAction": "Включить другие модели"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "Папки других моделей не найдены",
|
||||||
|
"descriptionStandalone": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте указанные ниже пути к папкам в settings.json и перезапустите LoRA Manager.",
|
||||||
|
"hintStandalone": "Сканируются только перечисленные выше ключи папок; ненужные ключи можно опустить.",
|
||||||
|
"descriptionComfyUI": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте соответствующие папки моделей в пути к моделям ComfyUI и перезагрузите эту страницу.",
|
||||||
|
"hintComfyUI": "Другие модели читаются из папок vae, upscale_models, text_encoders, clip_vision и controlnet в ComfyUI.",
|
||||||
|
"openSettings": "Открыть настройки"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "启用其他模型失败",
|
"enableFailed": "启用其他模型失败",
|
||||||
"downloadBlocked": "其他模型管理已对此模型类型禁用。请在“设置 > 库”中启用以下载此文件。",
|
"downloadBlocked": "其他模型管理已对此模型类型禁用。请在“设置 > 库”中启用以下载此文件。",
|
||||||
"enableAction": "启用其他模型"
|
"enableAction": "启用其他模型"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "未找到其他模型文件夹",
|
||||||
|
"descriptionStandalone": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将下面的文件夹路径添加到 settings.json,然后重启 LoRA Manager。",
|
||||||
|
"hintStandalone": "只会扫描上面列出的文件夹键;不需要的键可以省略。",
|
||||||
|
"descriptionComfyUI": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将对应的模型文件夹添加到 ComfyUI 的模型路径,然后重新加载此页面。",
|
||||||
|
"hintComfyUI": "其他模型从 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 文件夹中读取。",
|
||||||
|
"openSettings": "打开设置"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
"enableFailed": "啟用其他模型失敗",
|
"enableFailed": "啟用其他模型失敗",
|
||||||
"downloadBlocked": "其他模型管理已對此模型類型停用。請在「設定 > 模型庫」中啟用以下載此檔案。",
|
"downloadBlocked": "其他模型管理已對此模型類型停用。請在「設定 > 模型庫」中啟用以下載此檔案。",
|
||||||
"enableAction": "啟用其他模型"
|
"enableAction": "啟用其他模型"
|
||||||
|
},
|
||||||
|
"noPaths": {
|
||||||
|
"title": "找不到其他模型資料夾",
|
||||||
|
"descriptionStandalone": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將下方的資料夾路徑加入 settings.json,然後重新啟動 LoRA Manager。",
|
||||||
|
"hintStandalone": "只會掃描上方列出的資料夾鍵;不需要的鍵可以省略。",
|
||||||
|
"descriptionComfyUI": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將對應的模型資料夾加入 ComfyUI 的模型路徑,然後重新載入此頁面。",
|
||||||
|
"hintComfyUI": "其他模型會從 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 資料夾讀取。",
|
||||||
|
"openSettings": "開啟設定"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -1494,6 +1494,54 @@ class Config:
|
|||||||
self.other_roots = self._init_other_paths()
|
self.other_roots = self._init_other_paths()
|
||||||
self._rebuild_preview_roots()
|
self._rebuild_preview_roots()
|
||||||
|
|
||||||
|
def get_other_models_availability(self) -> Dict[str, Any]:
|
||||||
|
"""Report the other-model folders the host can actually expose.
|
||||||
|
|
||||||
|
Independent of the opt-in ``enable_other_models`` toggle: this answers
|
||||||
|
"could Other Models management work here at all?". ComfyUI mode almost
|
||||||
|
always has these folder keys registered, while standalone mode only
|
||||||
|
knows the keys present in ``settings.json.folder_paths`` - so the UI
|
||||||
|
uses this to decide whether announcing the feature would be actionable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``{"available": bool, "sub_types": {sub_type: [existing roots]}}``.
|
||||||
|
A folder only counts when it exists on disk; an empty folder still
|
||||||
|
counts because CivitAI downloads can target it.
|
||||||
|
"""
|
||||||
|
sub_types: Dict[str, List[str]] = {}
|
||||||
|
try:
|
||||||
|
keys = self._collapse_legacy_folder_keys(
|
||||||
|
list(OTHER_MODEL_FOLDER_SUBTYPES.keys())
|
||||||
|
)
|
||||||
|
except Exception: # pragma: no cover - defensive
|
||||||
|
keys = list(OTHER_MODEL_FOLDER_SUBTYPES.keys())
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
sub_type = OTHER_MODEL_FOLDER_SUBTYPES.get(key)
|
||||||
|
if not sub_type:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
raw_paths = folder_paths.get_folder_paths(key)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("Error probing folder paths for '%s': %s", key, exc)
|
||||||
|
continue
|
||||||
|
|
||||||
|
bucket = sub_types.setdefault(sub_type, [])
|
||||||
|
for root in sorted(
|
||||||
|
self._dedupe_existing_paths(raw_paths or []).values(),
|
||||||
|
key=lambda path: path.lower(),
|
||||||
|
):
|
||||||
|
if root not in bucket:
|
||||||
|
bucket.append(root)
|
||||||
|
|
||||||
|
available_sub_types = {
|
||||||
|
sub_type: roots for sub_type, roots in sub_types.items() if roots
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"available": bool(available_sub_types),
|
||||||
|
"sub_types": available_sub_types,
|
||||||
|
}
|
||||||
|
|
||||||
def get_preview_static_url(self, preview_path: str) -> str:
|
def get_preview_static_url(self, preview_path: str) -> str:
|
||||||
if not preview_path:
|
if not preview_path:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -1559,6 +1559,22 @@ class SettingsHandler:
|
|||||||
response_data["civitai_api_key_set"] = bool(raw_key)
|
response_data["civitai_api_key_set"] = bool(raw_key)
|
||||||
raw_llm_key = self._settings.get("llm_api_key")
|
raw_llm_key = self._settings.get("llm_api_key")
|
||||||
response_data["llm_api_key_set"] = bool(raw_llm_key)
|
response_data["llm_api_key_set"] = bool(raw_llm_key)
|
||||||
|
# Derived capability flag (not persisted): whether the host exposes
|
||||||
|
# any other-model folder at all. Standalone installs only know the
|
||||||
|
# folder_paths keys present in settings.json, so the announcement
|
||||||
|
# banner uses this to avoid promising a page that cannot list
|
||||||
|
# anything.
|
||||||
|
try:
|
||||||
|
availability = config.get_other_models_availability()
|
||||||
|
response_data["other_models_paths_available"] = bool(
|
||||||
|
availability.get("available")
|
||||||
|
)
|
||||||
|
except Exception as availability_error: # pragma: no cover - defensive
|
||||||
|
logger.debug(
|
||||||
|
"Could not resolve Other Models availability: %s",
|
||||||
|
availability_error,
|
||||||
|
)
|
||||||
|
response_data["other_models_paths_available"] = None
|
||||||
settings_file = getattr(self._settings, "settings_file", None)
|
settings_file = getattr(self._settings, "settings_file", None)
|
||||||
if settings_file:
|
if settings_file:
|
||||||
response_data["settings_file"] = settings_file
|
response_data["settings_file"] = settings_file
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
@@ -75,7 +76,18 @@ class OtherRoutes(BaseModelRoutes):
|
|||||||
return self._page_context_for_other
|
return self._page_context_for_other
|
||||||
|
|
||||||
def _page_context_for_other(self, request: web.Request) -> Dict[str, Any]:
|
def _page_context_for_other(self, request: web.Request) -> Dict[str, Any]:
|
||||||
return {"other_disabled": not self._settings.is_other_models_enabled()}
|
if not self._settings.is_other_models_enabled():
|
||||||
|
return {"other_disabled": True, "other_no_paths": False}
|
||||||
|
|
||||||
|
# Enabled but nothing to scan: folder paths for the managed sub_types
|
||||||
|
# resolved to no existing folder. Render an actionable empty state
|
||||||
|
# instead of an apparently broken empty grid.
|
||||||
|
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
|
||||||
|
return {
|
||||||
|
"other_disabled": False,
|
||||||
|
"other_no_paths": not bool(config.other_roots),
|
||||||
|
"standalone_mode": standalone_mode,
|
||||||
|
}
|
||||||
|
|
||||||
def _get_expected_model_types(self) -> str:
|
def _get_expected_model_types(self) -> str:
|
||||||
"""Get expected model types string for error messages"""
|
"""Get expected model types string for error messages"""
|
||||||
|
|||||||
@@ -553,6 +553,13 @@ class BannerService {
|
|||||||
if (state.global.settings.enable_other_models) {
|
if (state.global.settings.enable_other_models) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Only announce when the host can actually resolve other-model folders.
|
||||||
|
// Standalone installs only know the folder_paths keys present in
|
||||||
|
// settings.json, so announcing there would land the user on an empty
|
||||||
|
// page. `=== false` (not falsy) keeps older payloads working.
|
||||||
|
if (state.global.settings.other_models_paths_available === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.isBannerDismissed(OTHER_MODELS_BANNER_ID)) {
|
if (this.isBannerDismissed(OTHER_MODELS_BANNER_ID)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { appCore } from './core.js';
|
import { appCore } from './core.js';
|
||||||
import { showToast } from './utils/uiHelpers.js';
|
import { showToast } from './utils/uiHelpers.js';
|
||||||
import { enableOtherModels } from './utils/otherModels.js';
|
import { enableOtherModels, openOtherModelsSettings } from './utils/otherModels.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Other Models is an opt-in feature. While it is disabled this page renders an
|
* Other Models is an opt-in feature. While it is disabled this page renders an
|
||||||
* empty state whose button turns the feature on; the backend then rebuilds the
|
* empty state whose button turns the feature on; the backend then rebuilds the
|
||||||
* other-model roots and starts scanning, so a reload lands on the real page.
|
* other-model roots and starts scanning, so a reload lands on the real page.
|
||||||
|
*
|
||||||
|
* The same module backs the "enabled but no folders found" state, where the
|
||||||
|
* only useful action is jumping to Settings instead of enabling anything.
|
||||||
*/
|
*/
|
||||||
async function handleEnableClick() {
|
async function handleEnableClick() {
|
||||||
const button = document.getElementById('enableOtherModelsBtn');
|
const button = document.getElementById('enableOtherModelsBtn');
|
||||||
@@ -20,6 +23,15 @@ async function handleEnableClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open Settings on the Library section for the "no folders found" state, so a
|
||||||
|
* misconfigured install can be fixed without hand-editing unknown keys.
|
||||||
|
*/
|
||||||
|
function handleOpenSettingsClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
openOtherModelsSettings();
|
||||||
|
}
|
||||||
|
|
||||||
async function initializeOtherDisabledPage() {
|
async function initializeOtherDisabledPage() {
|
||||||
// appCore.initialize() wires the shared header (theme, settings modal,
|
// appCore.initialize() wires the shared header (theme, settings modal,
|
||||||
// language) so this page is not a dead end.
|
// language) so this page is not a dead end.
|
||||||
@@ -29,6 +41,11 @@ async function initializeOtherDisabledPage() {
|
|||||||
if (button) {
|
if (button) {
|
||||||
button.addEventListener('click', handleEnableClick);
|
button.addEventListener('click', handleEnableClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settingsButton = document.getElementById('openOtherModelsSettingsBtn');
|
||||||
|
if (settingsButton) {
|
||||||
|
settingsButton.addEventListener('click', handleOpenSettingsClick);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initializeOtherDisabledPage);
|
document.addEventListener('DOMContentLoaded', initializeOtherDisabledPage);
|
||||||
|
|||||||
+35
-1
@@ -51,6 +51,18 @@
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
.other-no-paths-config {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
max-width: 520px;
|
||||||
|
overflow-x: auto;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(127, 127, 127, 0.15);
|
||||||
|
border: 1px solid rgba(127, 127, 127, 0.25);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -109,6 +121,28 @@
|
|||||||
</button>
|
</button>
|
||||||
<p class="other-disabled-hint">{{ t('other.disabled.hint') }}</p>
|
<p class="other-disabled-hint">{{ t('other.disabled.hint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% elif other_no_paths %}
|
||||||
|
<div class="other-disabled">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
<h2>{{ t('other.noPaths.title') }}</h2>
|
||||||
|
{% if standalone_mode %}
|
||||||
|
<p>{{ t('other.noPaths.descriptionStandalone') }}</p>
|
||||||
|
<pre class="other-no-paths-config"><code>"folder_paths": {
|
||||||
|
"vae": ["/path/to/vae"],
|
||||||
|
"upscale_models": ["/path/to/upscale_models"],
|
||||||
|
"text_encoders": ["/path/to/text_encoders"],
|
||||||
|
"clip_vision": ["/path/to/clip_vision"],
|
||||||
|
"controlnet": ["/path/to/controlnet"]
|
||||||
|
}</code></pre>
|
||||||
|
<p class="other-disabled-hint">{{ t('other.noPaths.hintStandalone') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ t('other.noPaths.descriptionComfyUI') }}</p>
|
||||||
|
<p class="other-disabled-hint">{{ t('other.noPaths.hintComfyUI') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<button id="openOtherModelsSettingsBtn" type="button">
|
||||||
|
<i class="fas fa-cog"></i> {{ t('other.noPaths.openSettings') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="sticky-topbar">
|
<div class="sticky-topbar">
|
||||||
{% include 'components/controls.html' %}
|
{% include 'components/controls.html' %}
|
||||||
@@ -129,7 +163,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
{% if other_disabled %}
|
{% if other_disabled or other_no_paths %}
|
||||||
<script type="module" src="/loras_static/js/other_disabled.js?v={{ version }}"></script>
|
<script type="module" src="/loras_static/js/other_disabled.js?v={{ version }}"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<script type="module" src="/loras_static/js/other.js?v={{ version }}"></script>
|
<script type="module" src="/loras_static/js/other.js?v={{ version }}"></script>
|
||||||
|
|||||||
@@ -414,3 +414,74 @@ class TestOtherRootsWiring:
|
|||||||
config._rebuild_preview_roots()
|
config._rebuild_preview_roots()
|
||||||
|
|
||||||
assert config.is_preview_path_allowed(str(vae_dir / "model.preview.png"))
|
assert config.is_preview_path_allowed(str(vae_dir / "model.preview.png"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestOtherModelsAvailability:
|
||||||
|
"""Config.get_other_models_availability ignores the opt-in toggle.
|
||||||
|
|
||||||
|
It answers "could Other Models work here at all?", which the settings
|
||||||
|
payload and the announcement banner use to avoid promising a page that
|
||||||
|
cannot list anything.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _stub_folder_paths(self, monkeypatch, mapping):
|
||||||
|
def get_folder_paths(key):
|
||||||
|
value = mapping.get(key, [])
|
||||||
|
return [value] if isinstance(value, str) else list(value)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
config_module.folder_paths, "get_folder_paths", get_folder_paths
|
||||||
|
)
|
||||||
|
# No host alias rewriting: every key stays independently queryable,
|
||||||
|
# which is what the standalone mock does.
|
||||||
|
monkeypatch.delattr(config_module.folder_paths, "map_legacy", raising=False)
|
||||||
|
|
||||||
|
def test_reports_available_when_a_folder_exists(self, monkeypatch, tmp_path):
|
||||||
|
vae_dir = tmp_path / "vae"
|
||||||
|
vae_dir.mkdir()
|
||||||
|
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
|
||||||
|
|
||||||
|
# The feature stays off on purpose: availability must not depend on it.
|
||||||
|
get_settings_manager().set("enable_other_models", False)
|
||||||
|
|
||||||
|
availability = _make_config().get_other_models_availability()
|
||||||
|
|
||||||
|
assert availability["available"] is True
|
||||||
|
assert availability["sub_types"] == {"vae": [_normalize(str(vae_dir))]}
|
||||||
|
|
||||||
|
def test_counts_an_empty_but_existing_folder(self, monkeypatch, tmp_path):
|
||||||
|
vae_dir = tmp_path / "vae"
|
||||||
|
vae_dir.mkdir()
|
||||||
|
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
|
||||||
|
|
||||||
|
availability = _make_config().get_other_models_availability()
|
||||||
|
|
||||||
|
assert availability["available"] is True
|
||||||
|
|
||||||
|
def test_ignores_missing_folders(self, monkeypatch, tmp_path):
|
||||||
|
self._stub_folder_paths(
|
||||||
|
monkeypatch, {"vae": str(tmp_path / "does-not-exist")}
|
||||||
|
)
|
||||||
|
|
||||||
|
availability = _make_config().get_other_models_availability()
|
||||||
|
|
||||||
|
assert availability == {"available": False, "sub_types": {}}
|
||||||
|
|
||||||
|
def test_reports_unavailable_without_any_configuration(self, monkeypatch):
|
||||||
|
self._stub_folder_paths(monkeypatch, {})
|
||||||
|
|
||||||
|
availability = _make_config().get_other_models_availability()
|
||||||
|
|
||||||
|
assert availability == {"available": False, "sub_types": {}}
|
||||||
|
|
||||||
|
def test_merges_legacy_clip_key_into_text_encoder(self, monkeypatch, tmp_path):
|
||||||
|
clip_dir = tmp_path / "clip"
|
||||||
|
clip_dir.mkdir()
|
||||||
|
self._stub_folder_paths(monkeypatch, {"clip": [str(clip_dir)]})
|
||||||
|
|
||||||
|
availability = _make_config().get_other_models_availability()
|
||||||
|
|
||||||
|
assert availability["available"] is True
|
||||||
|
assert availability["sub_types"] == {
|
||||||
|
"text_encoder": [_normalize(str(clip_dir))]
|
||||||
|
}
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ describe('BannerService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.global.settings.enable_other_models = false;
|
state.global.settings.enable_other_models = false;
|
||||||
|
state.global.settings.other_models_paths_available = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('announces the feature while it is switched off', () => {
|
it('announces the feature while it is switched off', () => {
|
||||||
@@ -225,6 +226,25 @@ describe('BannerService', () => {
|
|||||||
.toContain('Other Models Management is available');
|
.toContain('Other Models Management is available');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('stays silent when the host exposes no other-model folders', () => {
|
||||||
|
// Standalone installs without the folder_paths keys in
|
||||||
|
// settings.json would land on an empty page, so do not announce.
|
||||||
|
state.global.settings.other_models_paths_available = false;
|
||||||
|
|
||||||
|
prepareBanner();
|
||||||
|
|
||||||
|
expect(bannerElement()).toBeNull();
|
||||||
|
expect(bannerService.banners.has(OTHER_MODELS_BANNER_ID)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still announces when availability is unknown (older payload)', () => {
|
||||||
|
delete state.global.settings.other_models_paths_available;
|
||||||
|
|
||||||
|
prepareBanner();
|
||||||
|
|
||||||
|
expect(bannerElement()).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('stays silent once the feature is enabled', () => {
|
it('stays silent once the feature is enabled', () => {
|
||||||
state.global.settings.enable_other_models = true;
|
state.global.settings.enable_other_models = true;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ describe('Other Models disabled page', () => {
|
|||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
initializeAppMock.mockResolvedValue(undefined);
|
initializeAppMock.mockResolvedValue(undefined);
|
||||||
document.body.innerHTML = '<button id="enableOtherModelsBtn"></button>';
|
document.body.innerHTML = [
|
||||||
|
'<button id="enableOtherModelsBtn"></button>',
|
||||||
|
'<button id="openOtherModelsSettingsBtn"></button>',
|
||||||
|
].join('');
|
||||||
|
|
||||||
Object.defineProperty(window, 'location', {
|
Object.defineProperty(window, 'location', {
|
||||||
value: { ...originalLocation, reload: vi.fn() },
|
value: { ...originalLocation, reload: vi.fn() },
|
||||||
@@ -43,12 +46,24 @@ describe('Other Models disabled page', () => {
|
|||||||
writable: true,
|
writable: true,
|
||||||
});
|
});
|
||||||
delete global.fetch;
|
delete global.fetch;
|
||||||
|
delete window.modalManager;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('boots the shared app core so the header stays usable', () => {
|
it('boots the shared app core so the header stays usable', () => {
|
||||||
expect(initializeAppMock).toHaveBeenCalledTimes(1);
|
expect(initializeAppMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens the Library settings from the no-folders state', () => {
|
||||||
|
const showModal = vi.fn();
|
||||||
|
window.modalManager = { showModal };
|
||||||
|
|
||||||
|
document.getElementById('openOtherModelsSettingsBtn').dispatchEvent(
|
||||||
|
new MouseEvent('click', { bubbles: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(showModal).toHaveBeenCalledWith('settingsModal');
|
||||||
|
});
|
||||||
|
|
||||||
it('enables Other Models through the settings API and reloads', async () => {
|
it('enables Other Models through the settings API and reloads', async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
'civitai_api_key_set': True,
|
'civitai_api_key_set': True,
|
||||||
'language': 'en',
|
'language': 'en',
|
||||||
'llm_api_key_set': False,
|
'llm_api_key_set': False,
|
||||||
|
'other_models_paths_available': False,
|
||||||
'theme': 'dark',
|
'theme': 'dark',
|
||||||
}),
|
}),
|
||||||
'success': True,
|
'success': True,
|
||||||
|
|||||||
@@ -98,17 +98,27 @@ def test_validate_rejects_switched_off_sub_type():
|
|||||||
assert handler._validate_civitai_model_type("Upscaler") is False
|
assert handler._validate_civitai_model_type("Upscaler") is False
|
||||||
|
|
||||||
|
|
||||||
def test_page_context_reports_feature_state():
|
def test_page_context_reports_feature_state(monkeypatch):
|
||||||
|
from py.config import config
|
||||||
from py.services.settings_manager import get_settings_manager
|
from py.services.settings_manager import get_settings_manager
|
||||||
|
|
||||||
manager = get_settings_manager()
|
manager = get_settings_manager()
|
||||||
handler = OtherRoutes()
|
handler = OtherRoutes()
|
||||||
provider = handler._get_page_context_provider()
|
provider = handler._get_page_context_provider()
|
||||||
|
|
||||||
assert provider(None) == {"other_disabled": False}
|
monkeypatch.setattr(config, "other_roots", ["/models/vae"], raising=False)
|
||||||
|
context = provider(None)
|
||||||
|
assert context["other_disabled"] is False
|
||||||
|
assert context["other_no_paths"] is False
|
||||||
|
|
||||||
|
# Enabled but nothing resolved: the page must explain how to fix it.
|
||||||
|
monkeypatch.setattr(config, "other_roots", [], raising=False)
|
||||||
|
context = provider(None)
|
||||||
|
assert context["other_disabled"] is False
|
||||||
|
assert context["other_no_paths"] is True
|
||||||
|
|
||||||
manager.set("enable_other_models", False)
|
manager.set("enable_other_models", False)
|
||||||
assert provider(None) == {"other_disabled": True}
|
assert provider(None) == {"other_disabled": True, "other_no_paths": False}
|
||||||
|
|
||||||
|
|
||||||
def test_get_expected_model_types_mentions_supported_types():
|
def test_get_expected_model_types_mentions_supported_types():
|
||||||
|
|||||||
Reference in New Issue
Block a user