feat(settings): editable model library paths for standalone mode

Standalone users previously had to hand-edit settings.json to configure
primary folder_paths. Add a standalone-only Model Paths section to the
settings modal:

- Backend exposes standalone_mode, folder_paths (with template placeholder
  values filtered out) and a data-driven folder_path_schema derived from
  OTHER_MODEL_FOLDER_SUBTYPES via GET /api/lm/settings
- The new section renders multi-path editors per model type from the
  schema, with inline enable_other_models / sub-type controls so other
  model types are configured without leaving the tab
- Persistent restart-required cues after a save: nav dot, inline notice
  and a global banner (unique id per change so dismissals don't mute
  future reminders)
- The missing-model-paths startup banner and the Other Models no-paths
  empty state now deep-link into the new section instead of pointing at
  settings.json
This commit is contained in:
Will Miao
2026-09-18 19:36:19 +08:00
parent d4b82d98b2
commit 5adfa3be36
27 changed files with 1566 additions and 66 deletions
+9
View File
@@ -192,6 +192,15 @@ The system runs in two modes:
- Auto-saves paths to `settings.json` in ComfyUI mode - Auto-saves paths to `settings.json` in ComfyUI mode
- `settings.json.example` is intentionally minimal (see Important Notes); all - `settings.json.example` is intentionally minimal (see Important Notes); all
other defaults live in `DEFAULT_SETTINGS` (`py/services/settings_manager.py`) other defaults live in `DEFAULT_SETTINGS` (`py/services/settings_manager.py`)
- **`folder_paths` vs `extra_folder_paths` — different purposes, do not conflate:**
- `folder_paths` (primary model roots): in ComfyUI plugin mode these come
from the ComfyUI host; in standalone mode they are the ONLY source of
model library paths and are currently edited by hand in `settings.json`.
- `extra_folder_paths` is a **ComfyUI-plugin-mode feature**: paths visible
ONLY to LoRA Manager, not to ComfyUI. Its motivation is that a very large
model library slows ComfyUI itself down, while LoRA Manager handles large
libraries without performance issues — so users keep ComfyUI's library
small and add the bulk via `extra_folder_paths`.
### Frontend UI Architecture ### Frontend UI Architecture
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "Allgemein", "general": "Allgemein",
"interface": "Oberfläche", "interface": "Oberfläche",
"library": "Bibliothek" "library": "Bibliothek",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "Einstellungen durchsuchen...", "placeholder": "Einstellungen durchsuchen...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "Dieser Pfad wird bereits für einen anderen Modelltyp verwendet. Bitte verwenden Sie separate Ordner für Checkpoints und Diffusionsmodelle." "checkpointUnetOverlapInline": "Dieser Pfad wird bereits für einen anderen Modelltyp verwendet. Bitte verwenden Sie separate Ordner für Checkpoints und Diffusionsmodelle."
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "Prioritäts-Tags", "title": "Prioritäts-Tags",
"description": "Passen Sie die Tag-Prioritätsreihenfolge für jeden Modelltyp an (z. B. character, concept, style(toon|toon_style))", "description": "Passen Sie die Tag-Prioritätsreihenfolge für jeden Modelltyp an (z. B. character, concept, style(toon|toon_style))",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "Keine Ordner für weitere Modelle gefunden", "title": "Keine Ordner für weitere Modelle gefunden",
"descriptionStandalone": "Die Verwaltung weiterer Modelle ist aktiviert, aber es wurden keine Ordner für weitere Modelle gefunden. Fügen Sie die benötigten Ordnerschlüssel zum Abschnitt folder_paths Ihrer settings.json hinzu und starten Sie LoRA Manager neu.", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "Nur die oben aufgeführten Ordnerschlüssel werden gescannt; nicht benötigte Schlüssel können weggelassen werden.", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"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.", "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.", "hintComfyUI": "Weitere Modelle werden aus den Ordnern vae, upscale_models, text_encoders, clip_vision und controlnet von ComfyUI gelesen.",
"openSettings": "Einstellungen öffnen", "openSettings": "Einstellungen öffnen",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "Einstellungsordner öffnen" "openSettingsFolder": "Einstellungsordner öffnen"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "General", "general": "General",
"interface": "Interface", "interface": "Interface",
"library": "Library" "library": "Library",
"modelPaths": "Model Paths"
}, },
"search": { "search": {
"placeholder": "Search settings...", "placeholder": "Search settings...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "This path is also used for a different model type. Use separate folders for checkpoints and diffusion models." "checkpointUnetOverlapInline": "This path is also used for a different model type. Use separate folders for checkpoints and diffusion models."
} }
}, },
"modelPaths": {
"title": "Model Library Paths",
"description": "Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "Requires restart to take effect",
"coreTypes": "Core Model Types",
"otherTypes": "Other Model Types",
"otherTypesDisabledHint": "No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "Restart required to apply path changes",
"pendingRestartBannerMessage": "Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "LoRA Paths",
"checkpoints": "Checkpoint Paths",
"unet": "Diffusion Model Paths",
"embeddings": "Embedding Paths",
"vae": "VAE Paths",
"upscale_models": "Upscaler Paths",
"text_encoders": "Text Encoder Paths",
"clip": "CLIP Paths (legacy)",
"clip_vision": "CLIP Vision Paths",
"controlnet": "ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "Priority Tags", "title": "Priority Tags",
"description": "Customize the tag priority order for each model type (e.g., character, concept, style(toon|toon_style))", "description": "Customize the tag priority order for each model type (e.g., character, concept, style(toon|toon_style))",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "No other-model folders found", "title": "No other-model folders found",
"descriptionStandalone": "Other Models management is on, but no other-model folders were found. Add the folder keys you need to the folder_paths section of your settings.json, then restart LoRA Manager.", "descriptionStandalone": "Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "Only the folder keys listed above are scanned; keys you do not need can be omitted.", "hintStandalone": "Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"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.", "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.", "hintComfyUI": "Other models are read from ComfyUI's vae, upscale_models, text_encoders, clip_vision and controlnet folders.",
"openSettings": "Open Settings", "openSettings": "Open Settings",
"openModelPaths": "Configure Model Folders",
"openSettingsFolder": "Open Settings Folder" "openSettingsFolder": "Open Settings Folder"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "General", "general": "General",
"interface": "Interfaz", "interface": "Interfaz",
"library": "Biblioteca" "library": "Biblioteca",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "Buscar ajustes...", "placeholder": "Buscar ajustes...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "Esta ruta ya se usa para otro tipo de modelo. Use carpetas separadas para checkpoints y modelos de difusión." "checkpointUnetOverlapInline": "Esta ruta ya se usa para otro tipo de modelo. Use carpetas separadas para checkpoints y modelos de difusión."
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "Etiquetas prioritarias", "title": "Etiquetas prioritarias",
"description": "Personaliza el orden de prioridad de etiquetas para cada tipo de modelo (p. ej., character, concept, style(toon|toon_style))", "description": "Personaliza el orden de prioridad de etiquetas para cada tipo de modelo (p. ej., character, concept, style(toon|toon_style))",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "No se encontraron carpetas de otros modelos", "title": "No se encontraron carpetas de otros modelos",
"descriptionStandalone": "La gestión de otros modelos está activada, pero no se encontraron carpetas de otros modelos. Añade las claves de carpeta que necesites a la sección folder_paths de tu settings.json y reinicia LoRA Manager.", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "Solo se escanean las claves de carpeta listadas arriba; las claves que no necesites puedes omitirlas.", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"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.", "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.", "hintComfyUI": "Los otros modelos se leen de las carpetas vae, upscale_models, text_encoders, clip_vision y controlnet de ComfyUI.",
"openSettings": "Abrir configuración", "openSettings": "Abrir configuración",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "Abrir carpeta de ajustes" "openSettingsFolder": "Abrir carpeta de ajustes"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "Général", "general": "Général",
"interface": "Interface", "interface": "Interface",
"library": "Bibliothèque" "library": "Bibliothèque",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "Rechercher dans les paramètres...", "placeholder": "Rechercher dans les paramètres...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "Ce chemin est déjà utilisé pour un autre type de modèle. Utilisez des dossiers séparés pour les checkpoints et les modèles de diffusion." "checkpointUnetOverlapInline": "Ce chemin est déjà utilisé pour un autre type de modèle. Utilisez des dossiers séparés pour les checkpoints et les modèles de diffusion."
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "Tags prioritaires", "title": "Tags prioritaires",
"description": "Personnalisez l'ordre de priorité des tags pour chaque type de modèle (par ex. : character, concept, style(toon|toon_style))", "description": "Personnalisez l'ordre de priorité des tags pour chaque type de modèle (par ex. : character, concept, style(toon|toon_style))",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "Aucun dossier dautres modèles trouvé", "title": "Aucun dossier dautres modèles trouvé",
"descriptionStandalone": "La gestion des autres modèles est activée, mais aucun dossier dautres modèles na été trouvé. Ajoutez les clés de dossiers dont vous avez besoin à la section folder_paths de votre settings.json, puis redémarrez LoRA Manager.", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "Seules les clés de dossiers listées ci-dessus sont analysées ; les clés inutiles peuvent être omises.", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"descriptionComfyUI": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés nexiste sur le disque. Ajoutez les dossiers de modèles correspondants à vos chemins de modèles ComfyUI, puis rechargez cette page.", "descriptionComfyUI": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés nexiste 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.", "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", "openSettings": "Ouvrir les paramètres",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "Ouvrir le dossier des paramètres" "openSettingsFolder": "Ouvrir le dossier des paramètres"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "כללי", "general": "כללי",
"interface": "ממשק", "interface": "ממשק",
"library": "ספרייה" "library": "ספרייה",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "חיפוש בהגדרות...", "placeholder": "חיפוש בהגדרות...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "הנתיב הזה כבר נמצא בשימוש עבור סוג מודל אחר. יש להשתמש בתיקיות נפרדות עבור checkpoints ומודלי דיפוזיה." "checkpointUnetOverlapInline": "הנתיב הזה כבר נמצא בשימוש עבור סוג מודל אחר. יש להשתמש בתיקיות נפרדות עבור checkpoints ומודלי דיפוזיה."
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "תגיות עדיפות", "title": "תגיות עדיפות",
"description": "התאם את סדר העדיפות של התגיות עבור כל סוג מודל (לדוגמה: character, concept, style(toon|toon_style))", "description": "התאם את סדר העדיפות של התגיות עבור כל סוג מודל (לדוגמה: character, concept, style(toon|toon_style))",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "לא נמצאו תיקיות של מודלים אחרים", "title": "לא נמצאו תיקיות של מודלים אחרים",
"descriptionStandalone": "ניהול המודלים האחרים פועל, אך לא נמצאו תיקיות של מודלים אחרים. הוסף את מפתחות התיקיות הדרושים למקטע folder_paths ב-settings.json והפעל מחדש את LoRA Manager.", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "רק מפתחות התיקיות המפורטים למעלה נסרקים; ניתן להשמיט מפתחות שאינך צריך.", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"descriptionComfyUI": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את תיקיות המודלים המתאימות לנתיבי המודלים של ComfyUI וטען מחדש עמוד זה.", "descriptionComfyUI": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את תיקיות המודלים המתאימות לנתיבי המודלים של ComfyUI וטען מחדש עמוד זה.",
"hintComfyUI": "מודלים אחרים נקראים מתיקיות vae, upscale_models, text_encoders, clip_vision ו-controlnet של ComfyUI.", "hintComfyUI": "מודלים אחרים נקראים מתיקיות vae, upscale_models, text_encoders, clip_vision ו-controlnet של ComfyUI.",
"openSettings": "פתח הגדרות", "openSettings": "פתח הגדרות",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "פתח תיקיית הגדרות" "openSettingsFolder": "פתח תיקיית הגדרות"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "一般", "general": "一般",
"interface": "インターフェース", "interface": "インターフェース",
"library": "ライブラリ" "library": "ライブラリ",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "設定を検索...", "placeholder": "設定を検索...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "このパスは別のモデルタイプですでに使用されています。Checkpoints と diffusion models には別々のフォルダを使用してください。" "checkpointUnetOverlapInline": "このパスは別のモデルタイプですでに使用されています。Checkpoints と diffusion models には別々のフォルダを使用してください。"
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "優先タグ", "title": "優先タグ",
"description": "各モデルタイプのタグ優先順位をカスタマイズします (例: character, concept, style(toon|toon_style))", "description": "各モデルタイプのタグ優先順位をカスタマイズします (例: character, concept, style(toon|toon_style))",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "その他のモデルのフォルダーが見つかりません", "title": "その他のモデルのフォルダーが見つかりません",
"descriptionStandalone": "その他のモデル管理はオンですが、その他のモデルのフォルダーが見つかりません。必要なフォルダーキーをsettings.jsonのfolder_pathsセクションに追加し、LoRA Managerを再起動してください。", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "スキャンされるのは上記のフォルダーキーのみです。不要なキーは省略できます。", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"descriptionComfyUI": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。該当するモデルフォルダーをComfyUIのモデルパスに追加し、このページを再読み込みしてください。", "descriptionComfyUI": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。該当するモデルフォルダーをComfyUIのモデルパスに追加し、このページを再読み込みしてください。",
"hintComfyUI": "その他のモデルは、ComfyUIのvae、upscale_models、text_encoders、clip_vision、controlnetフォルダーから読み込まれます。", "hintComfyUI": "その他のモデルは、ComfyUIのvae、upscale_models、text_encoders、clip_vision、controlnetフォルダーから読み込まれます。",
"openSettings": "設定を開く", "openSettings": "設定を開く",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "設定フォルダーを開く" "openSettingsFolder": "設定フォルダーを開く"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "일반", "general": "일반",
"interface": "인터페이스", "interface": "인터페이스",
"library": "라이브러리" "library": "라이브러리",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "설정 검색...", "placeholder": "설정 검색...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "이 경로는 다른 모델 유형에 이미 사용 중입니다. checkpoints와 diffusion models에 별도의 폴더를 사용하세요." "checkpointUnetOverlapInline": "이 경로는 다른 모델 유형에 이미 사용 중입니다. checkpoints와 diffusion models에 별도의 폴더를 사용하세요."
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "우선순위 태그", "title": "우선순위 태그",
"description": "모델 유형별 태그 우선순위를 사용자 지정합니다(예: character, concept, style(toon|toon_style)).", "description": "모델 유형별 태그 우선순위를 사용자 지정합니다(예: character, concept, style(toon|toon_style)).",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "기타 모델 폴더를 찾을 수 없습니다", "title": "기타 모델 폴더를 찾을 수 없습니다",
"descriptionStandalone": "기타 모델 관리가 켜져 있지만, 기타 모델 폴더를 찾을 수 없습니다. 필요한 폴더 키를 settings.json의 folder_paths 섹션에 추가한 뒤 LoRA Manager를 재시작하세요.", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "위에 나열된 폴더 키만 스캔됩니다. 필요 없는 키는 생략할 수 있습니다.", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"descriptionComfyUI": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 해당 모델 폴더를 ComfyUI 모델 경로에 추가한 뒤 이 페이지를 새로 고침하세요.", "descriptionComfyUI": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 해당 모델 폴더를 ComfyUI 모델 경로에 추가한 뒤 이 페이지를 새로 고침하세요.",
"hintComfyUI": "기타 모델은 ComfyUI의 vae, upscale_models, text_encoders, clip_vision, controlnet 폴더에서 읽어옵니다.", "hintComfyUI": "기타 모델은 ComfyUI의 vae, upscale_models, text_encoders, clip_vision, controlnet 폴더에서 읽어옵니다.",
"openSettings": "설정 열기", "openSettings": "설정 열기",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "설정 폴더 열기" "openSettingsFolder": "설정 폴더 열기"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "Общее", "general": "Общее",
"interface": "Интерфейс", "interface": "Интерфейс",
"library": "Библиотека" "library": "Библиотека",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "Поиск в настройках...", "placeholder": "Поиск в настройках...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "Этот путь уже используется для другого типа модели. Используйте отдельные папки для checkpoints и diffusion models." "checkpointUnetOverlapInline": "Этот путь уже используется для другого типа модели. Используйте отдельные папки для checkpoints и diffusion models."
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "Приоритетные теги", "title": "Приоритетные теги",
"description": "Настройте порядок приоритетов тегов для каждого типа моделей (например, character, concept, style(toon|toon_style)).", "description": "Настройте порядок приоритетов тегов для каждого типа моделей (например, character, concept, style(toon|toon_style)).",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "Папки других моделей не найдены", "title": "Папки других моделей не найдены",
"descriptionStandalone": "Управление другими моделями включено, но папки других моделей не найдены. Добавьте нужные ключи папок в раздел folder_paths файла settings.json и перезапустите LoRA Manager.", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "Сканируются только перечисленные выше ключи папок; ненужные ключи можно опустить.", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"descriptionComfyUI": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте соответствующие папки моделей в пути к моделям ComfyUI и перезагрузите эту страницу.", "descriptionComfyUI": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте соответствующие папки моделей в пути к моделям ComfyUI и перезагрузите эту страницу.",
"hintComfyUI": "Другие модели читаются из папок vae, upscale_models, text_encoders, clip_vision и controlnet в ComfyUI.", "hintComfyUI": "Другие модели читаются из папок vae, upscale_models, text_encoders, clip_vision и controlnet в ComfyUI.",
"openSettings": "Открыть настройки", "openSettings": "Открыть настройки",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "Открыть папку настроек" "openSettingsFolder": "Открыть папку настроек"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "通用", "general": "通用",
"interface": "界面", "interface": "界面",
"library": "库" "library": "库",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "搜索设置...", "placeholder": "搜索设置...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "此路径已被用于另一种模型类型。请为 checkpoints 和 diffusion models 使用不同的文件夹。" "checkpointUnetOverlapInline": "此路径已被用于另一种模型类型。请为 checkpoints 和 diffusion models 使用不同的文件夹。"
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "优先标签", "title": "优先标签",
"description": "为每种模型类型自定义标签优先级顺序 (例如: character, concept, style(toon|toon_style))", "description": "为每种模型类型自定义标签优先级顺序 (例如: character, concept, style(toon|toon_style))",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "未找到其他模型文件夹", "title": "未找到其他模型文件夹",
"descriptionStandalone": "其他模型管理已开启,但未找到其他模型文件夹。请将你需要的文件夹键添加到 settings.json 的 folder_paths 部分,然后重启 LoRA Manager", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "只会扫描上面列出的文件夹键;不需要的键可以省略。", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"descriptionComfyUI": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将对应的模型文件夹添加到 ComfyUI 的模型路径,然后重新加载此页面。", "descriptionComfyUI": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将对应的模型文件夹添加到 ComfyUI 的模型路径,然后重新加载此页面。",
"hintComfyUI": "其他模型从 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 文件夹中读取。", "hintComfyUI": "其他模型从 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 文件夹中读取。",
"openSettings": "打开设置", "openSettings": "打开设置",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "打开设置文件夹" "openSettingsFolder": "打开设置文件夹"
} }
}, },
+29 -3
View File
@@ -382,7 +382,8 @@
"nav": { "nav": {
"general": "通用", "general": "通用",
"interface": "介面", "interface": "介面",
"library": "模型庫" "library": "模型庫",
"modelPaths": "[TODO: Translate] Model Paths"
}, },
"search": { "search": {
"placeholder": "搜尋設定...", "placeholder": "搜尋設定...",
@@ -583,6 +584,30 @@
"checkpointUnetOverlapInline": "此路徑已被用於另一種模型類型。請為 checkpoints 和 diffusion models 使用不同的資料夾。" "checkpointUnetOverlapInline": "此路徑已被用於另一種模型類型。請為 checkpoints 和 diffusion models 使用不同的資料夾。"
} }
}, },
"modelPaths": {
"title": "[TODO: Translate] Model Library Paths",
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
"restartRequired": "[TODO: Translate] Requires restart to take effect",
"coreTypes": "[TODO: Translate] Core Model Types",
"otherTypes": "[TODO: Translate] Other Model Types",
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
"folderKeys": {
"loras": "[TODO: Translate] LoRA Paths",
"checkpoints": "[TODO: Translate] Checkpoint Paths",
"unet": "[TODO: Translate] Diffusion Model Paths",
"embeddings": "[TODO: Translate] Embedding Paths",
"vae": "[TODO: Translate] VAE Paths",
"upscale_models": "[TODO: Translate] Upscaler Paths",
"text_encoders": "[TODO: Translate] Text Encoder Paths",
"clip": "[TODO: Translate] CLIP Paths (legacy)",
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
"controlnet": "[TODO: Translate] ControlNet Paths"
}
},
"priorityTags": { "priorityTags": {
"title": "優先標籤", "title": "優先標籤",
"description": "為每種模型類型自訂標籤的優先順序 (例如: character, concept, style(toon|toon_style))", "description": "為每種模型類型自訂標籤的優先順序 (例如: character, concept, style(toon|toon_style))",
@@ -1241,11 +1266,12 @@
}, },
"noPaths": { "noPaths": {
"title": "找不到其他模型資料夾", "title": "找不到其他模型資料夾",
"descriptionStandalone": "其他模型管理已開啟,但找不到其他模型資料夾。請將您需要的資料夾鍵加入 settings.json 的 folder_paths 區段,然後重新啟動 LoRA Manager", "descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
"hintStandalone": "只會掃描上方列出的資料夾鍵;不需要的鍵可以省略。", "hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
"descriptionComfyUI": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將對應的模型資料夾加入 ComfyUI 的模型路徑,然後重新載入此頁面。", "descriptionComfyUI": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將對應的模型資料夾加入 ComfyUI 的模型路徑,然後重新載入此頁面。",
"hintComfyUI": "其他模型會從 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 資料夾讀取。", "hintComfyUI": "其他模型會從 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 資料夾讀取。",
"openSettings": "開啟設定", "openSettings": "開啟設定",
"openModelPaths": "[TODO: Translate] Configure Model Folders",
"openSettingsFolder": "開啟設定資料夾" "openSettingsFolder": "開啟設定資料夾"
} }
}, },
+25
View File
@@ -54,6 +54,7 @@ from ...utils.constants import (
SUPPORTED_MEDIA_EXTENSIONS, SUPPORTED_MEDIA_EXTENSIONS,
VALID_LORA_TYPES, VALID_LORA_TYPES,
VALID_OTHER_CIVITAI_TYPES, VALID_OTHER_CIVITAI_TYPES,
folder_path_schema,
) )
from .model_source_handlers import ModelSourceHandler from .model_source_handlers import ModelSourceHandler
from .agent_handlers import AgentHandler from .agent_handlers import AgentHandler
@@ -1580,6 +1581,30 @@ class SettingsHandler:
availability_error, availability_error,
) )
response_data["other_models_paths_available"] = None response_data["other_models_paths_available"] = None
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
response_data["standalone_mode"] = standalone_mode
if standalone_mode:
# Standalone reads its model roots exclusively from
# settings.json, so the Model Paths settings UI needs the
# current values plus the editable-key schema. In plugin mode
# the paths come from the ComfyUI host and stay hidden.
folder_paths = self._settings.get("folder_paths") or {}
# A fresh install is seeded from settings.json.example, whose
# folder_paths are documentation placeholders — hide them so
# the UI starts with empty editors instead of fake paths.
get_placeholders = getattr(
self._settings, "get_template_folder_path_placeholders", None
)
placeholders = get_placeholders() if get_placeholders else set()
if placeholders:
folder_paths = {
key: [p for p in paths if p not in placeholders]
if isinstance(paths, list)
else paths
for key, paths in folder_paths.items()
}
response_data["folder_paths"] = folder_paths
response_data["folder_path_schema"] = folder_path_schema()
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
+2 -2
View File
@@ -89,8 +89,8 @@ class OtherRoutes(BaseModelRoutes):
"standalone_mode": standalone_mode, "standalone_mode": standalone_mode,
} }
if standalone_mode: if standalone_mode:
# The settings UI cannot edit primary folder_paths, so the empty # The empty state points at the Model Paths settings section and
# state must point at the actual file the user has to edit. # shows the settings.json path as a fallback reference.
context["settings_file"] = getattr(self._settings, "settings_file", "") or "" context["settings_file"] = getattr(self._settings, "settings_file", "") or ""
return context return context
+35 -3
View File
@@ -19,6 +19,7 @@ from typing import (
Mapping, Mapping,
Optional, Optional,
Sequence, Sequence,
Set,
Tuple, Tuple,
) )
@@ -308,6 +309,29 @@ class SettingsManager:
return payload == template return payload == template
def get_template_folder_path_placeholders(self) -> Set[str]:
"""Placeholder folder_paths values shipped in settings.json.example.
A fresh standalone install is seeded from the template, so its
documentation-only placeholder paths end up in the live settings
file. The Model Paths settings UI hides them; the first real save
overwrites them via ``set("folder_paths")``.
"""
template = self._read_template_payload()
if not template:
return set()
folder_paths = template.get("folder_paths")
if not isinstance(folder_paths, Mapping):
return set()
placeholders: Set[str] = set()
for value in folder_paths.values():
paths = value if isinstance(value, list) else [value]
placeholders.update(p for p in paths if isinstance(p, str) and p)
return placeholders
def _merge_template_with_defaults( def _merge_template_with_defaults(
self, defaults: Dict[str, Any], template: Mapping[str, Any] self, defaults: Dict[str, Any], template: Mapping[str, Any]
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@@ -1219,19 +1243,27 @@ class SettingsManager:
if self._bootstrap_reason == "missing": if self._bootstrap_reason == "missing":
message = ( message = (
"LoRA Manager created a default settings.json because no configuration was found. " "LoRA Manager created a default settings.json because no configuration was found. "
"Edit settings.json to add your model directories so library scanning can run." "Open Settings → Model Paths to add your model directories so library scanning can run."
) )
else: else:
message = ( message = (
"LoRA Manager could not locate any configured model directories. " "LoRA Manager could not locate any configured model directories. "
"Edit settings.json to add your model folders so library scanning can run." "Open Settings → Model Paths to add your model folders so library scanning can run."
) )
self._add_startup_message( self._add_startup_message(
code="missing-model-paths", code="missing-model-paths",
title="Model folders need setup", title="Model folders need setup",
message=message, message=message,
severity="warning", severity="warning",
actions=self._default_settings_actions(), actions=[
{
"action": "open-model-paths-settings",
"label": "Configure model folders",
"type": "primary",
"icon": "fas fa-cog",
},
*self._default_settings_actions(),
],
dismissible=False, dismissible=False,
) )
+23
View File
@@ -127,6 +127,29 @@ def other_sub_type_folder_keys() -> Dict[str, List[str]]:
# Precomputed inverse of OTHER_MODEL_FOLDER_SUBTYPES, keeping the table order. # Precomputed inverse of OTHER_MODEL_FOLDER_SUBTYPES, keeping the table order.
OTHER_SUB_TYPE_FOLDER_KEYS: Dict[str, List[str]] = other_sub_type_folder_keys() OTHER_SUB_TYPE_FOLDER_KEYS: Dict[str, List[str]] = other_sub_type_folder_keys()
# Core folder_paths keys every LoRA Manager installation understands.
CORE_FOLDER_PATH_KEYS: List[str] = ["loras", "checkpoints", "unet", "embeddings"]
def folder_path_schema() -> List[Dict[str, Any]]:
"""Ordered schema describing the editable folder_paths keys.
Drives the standalone-only Model Paths settings UI: the frontend renders
one multi-path editor per entry and resolves labels via the
``settings.modelPaths.folderKeys.<key>`` i18n keys, so adding a new model
category is a constants + locale change only. ``sub_type`` lets the UI
hide editors for other-model categories the user has not enabled.
"""
schema: List[Dict[str, Any]] = [
{"key": key, "category": "core", "sub_type": None}
for key in CORE_FOLDER_PATH_KEYS
]
schema.extend(
{"key": folder_key, "category": "other", "sub_type": sub_type}
for folder_key, sub_type in OTHER_MODEL_FOLDER_SUBTYPES.items()
)
return schema
def normalize_other_sub_types(value: Any) -> List[str]: def normalize_other_sub_types(value: Any) -> List[str]:
"""Normalize a stored/requested enabled-sub_type list. """Normalize a stored/requested enabled-sub_type list.
@@ -1780,3 +1780,38 @@ input:checked + .toggle-slider:before {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
/* Standalone Model Paths: pending-restart cues */
.settings-nav-item.has-pending-restart {
position: relative;
}
.settings-nav-item.has-pending-restart::after {
content: '';
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--lora-warning, #e67e22);
}
.model-paths-restart-notice {
display: none;
margin-top: 8px;
padding: 10px 14px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--lora-warning, #e67e22);
background: rgba(230, 126, 34, 0.08);
color: var(--lora-warning, #e67e22);
font-size: 0.85em;
line-height: 1.4;
align-items: center;
gap: 8px;
}
.model-paths-restart-notice.visible {
display: flex;
}
+480 -3
View File
@@ -18,6 +18,16 @@ import { bannerService } from './BannerService.js';
const VALID_MATURE_BLUR_LEVELS = new Set(['PG13', 'R', 'X', 'XXX']); const VALID_MATURE_BLUR_LEVELS = new Set(['PG13', 'R', 'X', 'XXX']);
// Other-model sub_type -> i18n label key, mirroring the checkbox list in
// templates/components/modals/settings/library.html.
const OTHER_SUB_TYPE_LABEL_KEYS = {
vae: 'settings.folderSettings.subTypeVae',
upscaler: 'settings.folderSettings.subTypeUpscaler',
text_encoder: 'settings.folderSettings.subTypeTextEncoder',
clip_vision: 'settings.folderSettings.subTypeClipVision',
controlnet: 'settings.folderSettings.subTypeControlnet',
};
export class SettingsManager { export class SettingsManager {
constructor() { constructor() {
this.initialized = false; this.initialized = false;
@@ -26,6 +36,8 @@ export class SettingsManager {
this.availableLibraries = {}; this.availableLibraries = {};
this.activeLibrary = ''; this.activeLibrary = '';
this.registeredStartupBannerIds = new Set(); this.registeredStartupBannerIds = new Set();
this.modelPathsSectionInitialized = false;
this.modelPathsDirty = false;
// Add initialization to sync with modal state // Add initialization to sync with modal state
this.currentPage = document.body.dataset.page || 'loras'; this.currentPage = document.body.dataset.page || 'loras';
@@ -78,6 +90,7 @@ export class SettingsManager {
await this.applyLanguageSetting(); await this.applyLanguageSetting();
this.applyFrontendSettings(); this.applyFrontendSettings();
this.setupModelPathsSection();
} }
async applyLanguageSetting() { async applyLanguageSetting() {
@@ -276,6 +289,10 @@ export class SettingsManager {
case 'open-settings-modal': case 'open-settings-modal':
modalManager.showModal('settingsModal'); modalManager.showModal('settingsModal');
break; break;
case 'open-model-paths-settings':
modalManager.showModal('settingsModal');
document.querySelector('.settings-nav-item[data-section="modelPaths"]')?.click();
break;
case 'open-settings-location': case 'open-settings-location':
this.openSettingsFileLocation(); this.openSettingsFileLocation();
break; break;
@@ -472,8 +489,11 @@ export class SettingsManager {
const sectionId = item.dataset.section; const sectionId = item.dataset.section;
if (!sectionId) return; if (!sectionId) return;
// Hide all sections // Query live instead of using the captured NodeLists: the
sections.forEach(section => { // standalone Model Paths section is added after this
// initializer runs, and a stale snapshot would leave it
// active forever.
document.querySelectorAll('.settings-section').forEach(section => {
section.classList.remove('active'); section.classList.remove('active');
}); });
@@ -484,7 +504,7 @@ export class SettingsManager {
} }
// Update active nav state // Update active nav state
navItems.forEach(nav => nav.classList.remove('active')); document.querySelectorAll('.settings-nav-item').forEach(nav => nav.classList.remove('active'));
item.classList.add('active'); item.classList.add('active');
}); });
}); });
@@ -1160,6 +1180,9 @@ export class SettingsManager {
// Load extra folder paths // Load extra folder paths
this.loadExtraFolderPaths(); this.loadExtraFolderPaths();
// Load standalone model library paths (no-op in plugin mode)
this.loadModelPaths();
// Load language setting // Load language setting
const languageSelect = document.getElementById('languageSelect'); const languageSelect = document.getElementById('languageSelect');
if (languageSelect) { if (languageSelect) {
@@ -1942,6 +1965,460 @@ export class SettingsManager {
} }
} }
// --- Standalone Model Paths section --------------------------------------
// The section only exists in standalone mode, where primary folder_paths
// are read from settings.json instead of the ComfyUI host. Editors are
// rendered from the backend-provided folder_path_schema so new model
// categories appear automatically.
setupModelPathsSection() {
if (this.modelPathsSectionInitialized) return;
if (!state.global.settings.standalone_mode) return;
const navGroup = document.querySelector('.settings-nav-list .settings-nav-group');
const settingsForm = document.querySelector('.settings-form');
if (!navGroup || !settingsForm) return;
const navButton = document.createElement('button');
navButton.type = 'button';
navButton.className = 'settings-nav-item';
navButton.dataset.section = 'modelPaths';
navButton.textContent = translate('settings.nav.modelPaths', {}, 'Model Paths');
const section = document.createElement('div');
section.className = 'settings-section';
section.id = 'section-modelPaths';
section.dataset.section = 'modelPaths';
section.innerHTML = `
<div class="settings-subsection">
<div class="settings-subsection-header">
<h4>
${translate('settings.modelPaths.title', {}, 'Model Library Paths')}
<i class="fas fa-sync-alt restart-required-icon" title="${translate('settings.modelPaths.restartRequired', {}, 'Restart required for changes to take effect')}"></i>
</h4>
</div>
<div class="setting-item">
<div class="input-help">
${translate('settings.modelPaths.description', {}, 'Root folders LoRA Manager scans for your models. Changes take effect after restarting the server.')}
</div>
</div>
<div class="model-paths-restart-notice" id="modelPathsRestartNotice">
<i class="fas fa-exclamation-triangle"></i>
<span>${translate('settings.modelPaths.pendingRestartNotice', {}, 'Path changes saved. Restart LoRA Manager for them to take effect.')}</span>
</div>
<div class="settings-subsection-header">
<h4>${translate('settings.modelPaths.coreTypes', {}, 'Core Model Types')}</h4>
</div>
<div id="modelPathsCoreTypes"></div>
<div class="settings-subsection-header">
<h4>${translate('settings.modelPaths.otherTypes', {}, 'Other Model Types')}</h4>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="modelPathsEnableOtherModels">
${translate('settings.folderSettings.enableOtherModels', {}, 'Enable Other Models Management')}
<i class="fas fa-info-circle info-icon" data-tooltip="${translate('settings.folderSettings.enableOtherModelsHelp', {}, '')}"></i>
</label>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="modelPathsEnableOtherModels" onchange="settingsManager.handleModelPathsEnableOtherModels()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="setting-item other-subtype-toggles" id="modelPathsSubTypeToggles">
<div class="setting-row">
<div class="setting-info">
<label>
${translate('settings.folderSettings.otherSubTypes', {}, 'Enabled Model Types')}
<i class="fas fa-info-circle info-icon" data-tooltip="${translate('settings.folderSettings.otherSubTypesHelp', {}, '')}"></i>
</label>
</div>
<div class="setting-control other-subtype-checkboxes">
${this._buildModelPathSubTypeCheckboxes()}
</div>
</div>
</div>
<div class="setting-item" id="modelPathsOtherEmpty">
<div class="input-help">
${translate('settings.modelPaths.otherTypesDisabledHint', {}, 'No other model types are enabled. Turn on the types you need above to configure their folders.')}
</div>
</div>
<div id="modelPathsOtherTypes"></div>
</div>
`;
// The static nav items were bound by initializeNavigation() before the
// backend sync completed, so this dynamically added item carries its
// own handler with the same show-section behavior.
navButton.addEventListener('click', () => {
document.querySelectorAll('.settings-section').forEach((s) => s.classList.remove('active'));
section.classList.add('active');
document.querySelectorAll('.settings-nav-item').forEach((n) => n.classList.remove('active'));
navButton.classList.add('active');
});
navGroup.appendChild(navButton);
settingsForm.appendChild(section);
this.modelPathsSectionInitialized = true;
}
_buildModelPathSubTypeCheckboxes() {
const schema = state.global.settings.folder_path_schema || [];
const subTypes = [];
schema.forEach((entry) => {
if (entry.category === 'other' && entry.sub_type && !subTypes.includes(entry.sub_type)) {
subTypes.push(entry.sub_type);
}
});
return subTypes.map((subType) => {
const labelKey = OTHER_SUB_TYPE_LABEL_KEYS[subType];
const label = labelKey ? translate(labelKey, {}, subType) : subType;
return `
<label class="other-subtype-checkbox">
<input type="checkbox" value="${subType}"
data-model-paths-subtype="${subType}"
onchange="settingsManager.handleModelPathsSubTypeToggles()">
<span>${label}</span>
</label>
`;
}).join('');
}
/**
* Master toggle inside the standalone Model Paths section. Edits the same
* enable_other_models key as the Library tab control and keeps both in
* sync, then re-renders the other-model path editors in place.
*/
async handleModelPathsEnableOtherModels() {
const toggle = document.getElementById('modelPathsEnableOtherModels');
if (!toggle) return;
const enabled = toggle.checked;
const previous = !!state.global.settings.enable_other_models;
try {
await this.saveSetting('enable_other_models', enabled);
// Mirror the Library tab flow: refresh its controls and roots,
// then re-render this section's editors immediately.
this.updateOtherModelsControls();
await this.loadOtherRoots();
this.updateOtherModelsControls();
this.updateOtherModelsNavVisibility(enabled);
this.removeOtherModelsAnnouncement(enabled);
this.loadModelPaths();
showToast('toast.settings.settingsUpdated', { setting: 'enable other models' }, 'success');
} catch (error) {
toggle.checked = previous;
state.global.settings.enable_other_models = previous;
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
}
}
/**
* Sub-type checkboxes inside the standalone Model Paths section. Saves the
* same enabled_other_sub_types allow-list as the Library tab checkboxes
* (which use data-other-subtype-toggle, so the two never mix) and
* re-renders the editors without requiring a modal reopen.
*/
async handleModelPathsSubTypeToggles() {
const values = Array.from(document.querySelectorAll('[data-model-paths-subtype]'))
.filter((input) => input.checked)
.map((input) => input.value);
const previous = state.global.settings.enabled_other_sub_types;
try {
await this.saveSetting('enabled_other_sub_types', values);
this.updateOtherModelsControls();
await this.loadOtherRoots();
this.updateOtherModelsControls();
this.loadModelPaths();
showToast('toast.settings.settingsUpdated', { setting: 'other model types' }, 'success');
} catch (error) {
state.global.settings.enabled_other_sub_types = previous;
this.loadModelPaths();
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
}
}
loadModelPaths() {
if (!state.global.settings.standalone_mode) return;
const coreHost = document.getElementById('modelPathsCoreTypes');
const otherHost = document.getElementById('modelPathsOtherTypes');
if (!coreHost || !otherHost) return;
coreHost.innerHTML = '';
otherHost.innerHTML = '';
const schema = state.global.settings.folder_path_schema || [];
const otherModelsEnabled = state.global.settings.enable_other_models === true;
const enabledSubTypes = new Set(state.global.settings.enabled_other_sub_types || []);
// Keep the inline enable controls in sync with the current settings.
const masterToggle = document.getElementById('modelPathsEnableOtherModels');
if (masterToggle) {
masterToggle.checked = otherModelsEnabled;
}
document.querySelectorAll('[data-model-paths-subtype]').forEach((input) => {
input.checked = enabledSubTypes.has(input.value);
input.disabled = !otherModelsEnabled;
});
const subTypeToggles = document.getElementById('modelPathsSubTypeToggles');
if (subTypeToggles) {
subTypeToggles.classList.toggle('is-disabled', !otherModelsEnabled);
}
let otherCount = 0;
schema.forEach((entry) => {
if (entry.category === 'core') {
this._buildModelPathTypeGroup(coreHost, entry);
} else if (otherModelsEnabled && entry.sub_type && enabledSubTypes.has(entry.sub_type)) {
otherCount++;
this._buildModelPathTypeGroup(otherHost, entry);
}
});
const emptyHint = document.getElementById('modelPathsOtherEmpty');
if (emptyHint) {
emptyHint.style.display = otherCount === 0 ? 'block' : 'none';
}
const folderPaths = state.global.settings.folder_paths || {};
schema.forEach((entry) => {
const container = document.getElementById(`modelFolderPaths-${entry.key}`);
if (!container) return;
container.innerHTML = '';
const paths = folderPaths[entry.key] || [];
paths.forEach((path) => {
this.addModelFolderPathRow(entry.key, path);
});
// No trailing empty row on load: an unconfigured type shows just
// its Add button, and removing a row never resurrects an empty one.
});
}
_buildModelPathTypeGroup(host, entry) {
const item = document.createElement('div');
item.className = 'setting-item';
const label = translate(`settings.modelPaths.folderKeys.${entry.key}`, {}, entry.key);
item.innerHTML = `
<div class="setting-row">
<div class="setting-info">
<label>${label}</label>
</div>
<div class="setting-control">
<button type="button" class="add-mapping-btn" onclick="settingsManager.addModelFolderPathRow('${entry.key}')">
<i class="fas fa-plus"></i>
<span>${translate('common.actions.add', {}, 'Add')}</span>
</button>
</div>
</div>
<div class="extra-folder-paths-container" id="modelFolderPaths-${entry.key}">
</div>
`;
host.appendChild(item);
}
addModelFolderPathRow(key, path = '', shouldFocus = true) {
const container = document.getElementById(`modelFolderPaths-${key}`);
if (!container) return;
const row = document.createElement('div');
row.className = 'extra-folder-path-row mapping-row';
row.innerHTML = `
<div class="path-controls">
<input type="text" class="extra-folder-path-input"
placeholder="${translate('settings.extraFolderPaths.pathPlaceholder', {}, '/path/to/models')}" value="${path}"
onblur="settingsManager.updateModelFolderPaths('${key}')"
onfocus="settingsManager.clearModelFolderPathError(this)"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
<button type="button" class="remove-path-btn"
onclick="settingsManager.removeModelFolderPathRow(this, '${key}')"
title="${translate('common.actions.delete', {}, 'Delete')}">
<i class="fas fa-times"></i>
</button>
</div>
<div class="extra-folder-path-error"></div>
`;
container.appendChild(row);
if (!path && shouldFocus) {
const input = row.querySelector('.extra-folder-path-input');
if (input) {
setTimeout(() => input.focus(), 0);
}
}
}
clearModelFolderPathError(input) {
input.classList.remove('has-error');
const row = input.closest('.extra-folder-path-row');
if (row) {
const errEl = row.querySelector('.extra-folder-path-error');
if (errEl) {
errEl.classList.remove('visible');
errEl.textContent = '';
}
}
}
_clearAllModelFolderPathErrors() {
const section = document.getElementById('section-modelPaths');
if (!section) return;
section.querySelectorAll('.extra-folder-path-input.has-error').forEach((input) => {
input.classList.remove('has-error');
});
section.querySelectorAll('.extra-folder-path-error.visible').forEach((el) => {
el.classList.remove('visible');
el.textContent = '';
});
}
_markModelFolderPathsError(key, overlappingPaths, showMessage = false) {
const container = document.getElementById(`modelFolderPaths-${key}`);
if (!container) return;
const inputs = container.querySelectorAll('.extra-folder-path-input');
inputs.forEach((input) => {
const val = input.value.trim();
if (val && overlappingPaths.includes(val)) {
input.classList.add('has-error');
if (showMessage) {
const row = input.closest('.extra-folder-path-row');
if (row) {
const errEl = row.querySelector('.extra-folder-path-error');
if (errEl) {
errEl.textContent = translate('settings.extraFolderPaths.validation.checkpointUnetOverlapInline', {}, 'This path is also used for a different model type. Use separate folders for checkpoints and diffusion models.');
errEl.classList.add('visible');
}
}
}
}
});
}
removeModelFolderPathRow(btn, key) {
const row = btn.closest('.extra-folder-path-row');
if (row) {
row.remove();
this.updateModelFolderPaths(key, { fromRemoval: true });
}
}
async updateModelFolderPaths(changedKey, { fromRemoval = false } = {}) {
this._clearAllModelFolderPathErrors();
const folderPaths = {};
const section = document.getElementById('section-modelPaths');
if (!section) return;
section.querySelectorAll('.extra-folder-paths-container[id^="modelFolderPaths-"]').forEach((container) => {
const key = container.id.slice('modelFolderPaths-'.length);
const paths = [];
container.querySelectorAll('.extra-folder-path-input').forEach((input) => {
const value = input.value.trim();
if (value) {
paths.push(value);
}
});
folderPaths[key] = paths;
});
// Client-side pre-check: checkpoints and unet must not share the same path.
const normalise = (p) => p.replace(/[/\\]+$/, '').toLowerCase();
const ckptSet = new Set((folderPaths.checkpoints || []).map(normalise));
const unetSet = new Set((folderPaths.unet || []).map(normalise));
const ckptOverlap = (folderPaths.checkpoints || []).filter(p => p && unetSet.has(normalise(p)));
const unetOverlap = (folderPaths.unet || []).filter(p => p && ckptSet.has(normalise(p)));
const hasOverlap = ckptOverlap.length > 0 || unetOverlap.length > 0;
if (hasOverlap) {
if (changedKey === 'checkpoints') {
this._markModelFolderPathsError('checkpoints', ckptOverlap, true);
this._markModelFolderPathsError('unet', unetOverlap, false);
} else if (changedKey === 'unet') {
this._markModelFolderPathsError('unet', unetOverlap, true);
this._markModelFolderPathsError('checkpoints', ckptOverlap, false);
} else {
this._markModelFolderPathsError('checkpoints', ckptOverlap, false);
this._markModelFolderPathsError('unet', unetOverlap, false);
}
return;
}
const currentPaths = state.global.settings.folder_paths || {};
const pathsChanged = JSON.stringify(currentPaths) !== JSON.stringify(folderPaths);
if (!pathsChanged) {
return;
}
state.global.settings.folder_paths = folderPaths;
try {
await this.saveSetting('folder_paths', folderPaths);
this._markModelPathsDirty();
showToast('settings.modelPaths.saveSuccessRestart', {}, 'success');
// Keep the continuous-add flow: after the user fills the trailing
// empty row, append a fresh one — but never after a removal.
const container = document.getElementById(`modelFolderPaths-${changedKey}`);
if (container && !fromRemoval) {
const inputs = container.querySelectorAll('.extra-folder-path-input');
const hasEmptyRow = Array.from(inputs).some((input) => !input.value.trim());
if (!hasEmptyRow) {
this.addModelFolderPathRow(changedKey, '');
}
}
} catch (error) {
console.error('Failed to save folder paths:', error);
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
state.global.settings.folder_paths = currentPaths;
this.loadModelPaths();
}
}
/**
* Surface a persistent "restart required" cue after folder_paths changes:
* a dot on the Model Paths nav item, an inline notice in the section, and
* a global banner. The banner id is unique per change because dismissed
* banner ids persist across restarts reusing one would mute future
* reminders. The whole state clears on the next page load (i.e. after the
* restart the user was asked to do).
*/
_markModelPathsDirty() {
if (this.modelPathsDirty) return;
this.modelPathsDirty = true;
document.querySelector('.settings-nav-item[data-section="modelPaths"]')
?.classList.add('has-pending-restart');
document.getElementById('modelPathsRestartNotice')?.classList.add('visible');
const bannerId = `model-paths-restart-${Date.now()}`;
bannerService.registerBanner(bannerId, {
id: bannerId,
title: translate('settings.modelPaths.pendingRestartBannerTitle', {}, 'Restart required to apply path changes'),
content: translate('settings.modelPaths.pendingRestartBannerMessage', {}, 'Model library paths were updated. Restart the LoRA Manager server to scan the new folders.'),
dismissible: true,
priority: 60,
});
}
loadBaseModelMappings() { loadBaseModelMappings() {
const mappingsContainer = document.getElementById('baseModelMappingsContainer'); const mappingsContainer = document.getElementById('baseModelMappingsContainer');
if (!mappingsContainer) return; if (!mappingsContainer) return;
+19 -6
View File
@@ -1,6 +1,6 @@
import { appCore } from './core.js'; import { appCore } from './core.js';
import { showToast } from './utils/uiHelpers.js'; import { showToast } from './utils/uiHelpers.js';
import { enableOtherModels, openOtherModelsSettings } from './utils/otherModels.js'; import { enableOtherModels, openOtherModelsSettings, openModelPathsSettings } 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
@@ -9,8 +9,8 @@ import { enableOtherModels, openOtherModelsSettings } from './utils/otherModels.
* *
* The same module backs the "enabled but no folders found" state: ComfyUI * The same module backs the "enabled but no folders found" state: ComfyUI
* mode points to the Settings page's Library section, while standalone mode * mode points to the Settings page's Library section, while standalone mode
* (where the settings UI cannot edit primary folder paths) reveals the * points to the standalone-only Model Paths section (which edits the primary
* settings.json file the user must edit instead. * folder_paths) and still offers the settings.json location as a fallback.
*/ */
async function handleEnableClick() { async function handleEnableClick() {
const button = document.getElementById('enableOtherModelsBtn'); const button = document.getElementById('enableOtherModelsBtn');
@@ -35,9 +35,17 @@ function handleOpenSettingsClick(event) {
} }
/** /**
* Open the settings.json location from the standalone no-folders state. * Open Settings on the Model Paths section for the standalone "no folders
* The settings UI cannot edit primary folder_paths, so the only useful * found" state, so the missing folders can be added directly.
* action is revealing the file itself (or copying its path in Docker). */
function handleOpenModelPathsSettingsClick(event) {
event.preventDefault();
openModelPathsSettings();
}
/**
* Open the settings.json location from the standalone no-folders state,
* offered as a fallback next to the Model Paths settings button.
*/ */
async function handleOpenSettingsFolderClick() { async function handleOpenSettingsFolderClick() {
const button = document.getElementById('openSettingsFolderBtn'); const button = document.getElementById('openSettingsFolderBtn');
@@ -84,6 +92,11 @@ async function initializeOtherDisabledPage() {
settingsButton.addEventListener('click', handleOpenSettingsClick); settingsButton.addEventListener('click', handleOpenSettingsClick);
} }
const modelPathsButton = document.getElementById('openModelPathsSettingsBtn');
if (modelPathsButton) {
modelPathsButton.addEventListener('click', handleOpenModelPathsSettingsClick);
}
const settingsFolderButton = document.getElementById('openSettingsFolderBtn'); const settingsFolderButton = document.getElementById('openSettingsFolderBtn');
if (settingsFolderButton) { if (settingsFolderButton) {
settingsFolderButton.addEventListener('click', handleOpenSettingsFolderClick); settingsFolderButton.addEventListener('click', handleOpenSettingsFolderClick);
+6
View File
@@ -77,6 +77,12 @@ export function createDefaultSettings() {
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG }, priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
default_other_roots: {}, default_other_roots: {},
enabled_other_sub_types: ['vae', 'upscaler', 'text_encoder'], enabled_other_sub_types: ['vae', 'upscaler', 'text_encoder'],
// Standalone-only fields populated by GET /api/lm/settings; in plugin
// mode the backend omits folder_paths/folder_path_schema and these
// defaults apply.
standalone_mode: false,
folder_paths: {},
folder_path_schema: [],
}; };
} }
+16
View File
@@ -50,3 +50,19 @@ export function openOtherModelsSettings() {
}); });
}, 100); }, 100);
} }
/**
* Open the settings modal on the standalone-only Model Paths section, where
* primary folder_paths are edited. The section only exists in standalone mode,
* so the nav item lookup simply no-ops elsewhere.
*/
export function openModelPathsSettings() {
const modalManager = window.modalManager;
if (modalManager && typeof modalManager.showModal === 'function') {
modalManager.showModal('settingsModal');
}
window.setTimeout(() => {
document.querySelector('.settings-nav-item[data-section="modelPaths"]')?.click();
}, 100);
}
+4 -20
View File
@@ -51,18 +51,6 @@
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);
}
.other-settings-file { .other-settings-file {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -140,17 +128,13 @@
<h2>{{ t('other.noPaths.title') }}</h2> <h2>{{ t('other.noPaths.title') }}</h2>
{% if standalone_mode %} {% if standalone_mode %}
<p>{{ t('other.noPaths.descriptionStandalone') }}</p> <p>{{ t('other.noPaths.descriptionStandalone') }}</p>
<p class="other-disabled-hint">{{ t('other.noPaths.hintStandalone') }}</p>
<button id="openModelPathsSettingsBtn" type="button">
<i class="fas fa-cog"></i> {{ t('other.noPaths.openModelPaths') }}
</button>
{% if settings_file %} {% if settings_file %}
<p class="other-settings-file"><i class="fas fa-file-alt"></i> <code>{{ settings_file }}</code></p> <p class="other-settings-file"><i class="fas fa-file-alt"></i> <code>{{ settings_file }}</code></p>
{% endif %} {% endif %}
<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>
<button id="openSettingsFolderBtn" type="button"> <button id="openSettingsFolderBtn" type="button">
<i class="fas fa-folder-open"></i> {{ t('other.noPaths.openSettingsFolder') }} <i class="fas fa-folder-open"></i> {{ t('other.noPaths.openSettingsFolder') }}
</button> </button>
@@ -0,0 +1,418 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
vi.mock('../../../static/js/managers/ModalManager.js', () => ({
modalManager: {
closeModal: vi.fn(),
showModal: vi.fn(),
},
}));
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
showToast: vi.fn(),
}));
vi.mock('../../../static/js/state/index.js', () => {
return {
state: {
global: {
settings: {},
},
},
createDefaultSettings: () => ({
language: 'en',
standalone_mode: false,
folder_paths: {},
folder_path_schema: [],
}),
};
});
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
resetAndReload: vi.fn(),
}));
vi.mock('../../../static/js/utils/constants.js', () => ({
DOWNLOAD_PATH_TEMPLATES: {},
DEFAULT_PATH_TEMPLATES: {},
MAPPABLE_BASE_MODELS: [],
PATH_TEMPLATE_PLACEHOLDERS: {},
DEFAULT_PRIORITY_TAG_CONFIG: {},
getMappableBaseModelsDynamic: () => [],
}));
vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({
translate: (_key, _params, fallback) => fallback ?? '',
}));
vi.mock('../../../static/js/i18n/index.js', () => ({
i18n: {
getCurrentLocale: () => 'en',
setLanguage: vi.fn().mockResolvedValue(),
},
}));
vi.mock('../../../static/js/components/shared/ModelCard.js', () => ({
configureModelCardVideo: vi.fn(),
}));
import { SettingsManager } from '../../../static/js/managers/SettingsManager.js';
import { bannerService } from '../../../static/js/managers/BannerService.js';
import { state } from '../../../static/js/state/index.js';
const CORE_SCHEMA = [
{ key: 'loras', category: 'core', sub_type: null },
{ key: 'checkpoints', category: 'core', sub_type: null },
{ key: 'unet', category: 'core', sub_type: null },
{ key: 'embeddings', category: 'core', sub_type: null },
];
const OTHER_SCHEMA = [
{ key: 'vae', category: 'other', sub_type: 'vae' },
{ key: 'controlnet', category: 'other', sub_type: 'controlnet' },
];
const createManager = () => {
const initSettingsSpy = vi
.spyOn(SettingsManager.prototype, 'initializeSettings')
.mockResolvedValue();
const initializeSpy = vi
.spyOn(SettingsManager.prototype, 'initialize')
.mockImplementation(() => {});
const manager = new SettingsManager();
initSettingsSpy.mockRestore();
initializeSpy.mockRestore();
return manager;
};
const buildModalDom = () => {
document.body.innerHTML = `
<nav class="settings-nav">
<ul class="settings-nav-list">
<li class="settings-nav-group">
<button type="button" class="settings-nav-item active" data-section="general">General</button>
</li>
</ul>
</nav>
<div class="settings-form">
<div class="settings-section active" id="section-general" data-section="general"></div>
</div>
`;
};
const setStandaloneSettings = (overrides = {}) => {
state.global.settings = {
standalone_mode: true,
folder_paths: {},
folder_path_schema: [...CORE_SCHEMA, ...OTHER_SCHEMA],
enable_other_models: false,
enabled_other_sub_types: [],
...overrides,
};
};
beforeEach(() => {
document.body.innerHTML = '';
vi.clearAllMocks();
bannerService.banners.clear();
});
afterEach(() => {
delete global.fetch;
});
describe('SettingsManager Model Paths section', () => {
it('does not create the section in plugin mode', () => {
buildModalDom();
state.global.settings = { standalone_mode: false };
const manager = createManager();
manager.setupModelPathsSection();
expect(document.querySelector('.settings-nav-item[data-section="modelPaths"]')).toBeNull();
expect(document.getElementById('section-modelPaths')).toBeNull();
});
it('creates nav item and section in standalone mode', () => {
buildModalDom();
setStandaloneSettings();
const manager = createManager();
manager.setupModelPathsSection();
expect(document.querySelector('.settings-nav-item[data-section="modelPaths"]')).not.toBeNull();
expect(document.getElementById('section-modelPaths')).not.toBeNull();
});
it('switches sections when the nav item is clicked', () => {
buildModalDom();
setStandaloneSettings();
const manager = createManager();
manager.setupModelPathsSection();
document.querySelector('.settings-nav-item[data-section="modelPaths"]').click();
expect(document.getElementById('section-modelPaths').classList.contains('active')).toBe(true);
expect(document.getElementById('section-general').classList.contains('active')).toBe(false);
});
it('static nav clicks clear the Model Paths active state (regression)', () => {
buildModalDom();
setStandaloneSettings();
const manager = createManager();
manager.setupModelPathsSection();
// Static nav items were bound before the Model Paths button existed;
// their handler must still clear its active state.
manager.initializeNavigation();
const modelPathsNav = document.querySelector('.settings-nav-item[data-section="modelPaths"]');
modelPathsNav.click();
expect(modelPathsNav.classList.contains('active')).toBe(true);
document.querySelector('.settings-nav-item[data-section="general"]').click();
expect(modelPathsNav.classList.contains('active')).toBe(false);
expect(document.getElementById('section-modelPaths').classList.contains('active')).toBe(false);
expect(document.getElementById('section-general').classList.contains('active')).toBe(true);
});
it('renders core editors and only enabled other-model editors', () => {
buildModalDom();
setStandaloneSettings({
enable_other_models: true,
enabled_other_sub_types: ['vae'],
folder_paths: { loras: ['/models/loras'] },
});
const manager = createManager();
manager.setupModelPathsSection();
manager.loadModelPaths();
// Core editors always rendered
CORE_SCHEMA.forEach(({ key }) => {
expect(document.getElementById(`modelFolderPaths-${key}`)).not.toBeNull();
});
// Only the enabled other-model sub-type is rendered
expect(document.getElementById('modelFolderPaths-vae')).not.toBeNull();
expect(document.getElementById('modelFolderPaths-controlnet')).toBeNull();
expect(document.getElementById('modelPathsOtherEmpty').style.display).toBe('none');
// Existing values populate rows
const loraInput = document.querySelector('#modelFolderPaths-loras .extra-folder-path-input');
expect(loraInput.value).toBe('/models/loras');
});
it('shows the empty hint when no other-model types are enabled', () => {
buildModalDom();
setStandaloneSettings();
const manager = createManager();
manager.setupModelPathsSection();
manager.loadModelPaths();
expect(document.getElementById('modelPathsOtherEmpty').style.display).toBe('block');
expect(document.getElementById('modelFolderPaths-vae')).toBeNull();
});
it('renders inline enable controls synced with current settings', () => {
buildModalDom();
setStandaloneSettings({
enable_other_models: true,
enabled_other_sub_types: ['vae'],
});
const manager = createManager();
manager.setupModelPathsSection();
manager.loadModelPaths();
const master = document.getElementById('modelPathsEnableOtherModels');
expect(master).not.toBeNull();
expect(master.checked).toBe(true);
const vaeBox = document.querySelector('[data-model-paths-subtype="vae"]');
const controlnetBox = document.querySelector('[data-model-paths-subtype="controlnet"]');
expect(vaeBox.checked).toBe(true);
expect(vaeBox.disabled).toBe(false);
expect(controlnetBox.checked).toBe(false);
});
it('inline master toggle saves the setting and re-renders editors', async () => {
buildModalDom();
setStandaloneSettings({
enable_other_models: true,
enabled_other_sub_types: ['vae'],
});
const manager = createManager();
manager.saveSetting = vi.fn().mockImplementation(async (key, value) => {
state.global.settings[key] = value;
});
manager.loadOtherRoots = vi.fn().mockResolvedValue();
manager.setupModelPathsSection();
manager.loadModelPaths();
expect(document.getElementById('modelFolderPaths-vae')).not.toBeNull();
const master = document.getElementById('modelPathsEnableOtherModels');
master.checked = false;
await manager.handleModelPathsEnableOtherModels();
expect(manager.saveSetting).toHaveBeenCalledWith('enable_other_models', false);
expect(state.global.settings.enable_other_models).toBe(false);
// Editors removed in place, empty hint back
expect(document.getElementById('modelFolderPaths-vae')).toBeNull();
expect(document.getElementById('modelPathsOtherEmpty').style.display).toBe('block');
});
it('inline sub-type checkboxes save the allow-list and re-render editors', async () => {
buildModalDom();
setStandaloneSettings({
enable_other_models: true,
enabled_other_sub_types: ['vae'],
});
const manager = createManager();
manager.saveSetting = vi.fn().mockImplementation(async (key, value) => {
state.global.settings[key] = value;
});
manager.loadOtherRoots = vi.fn().mockResolvedValue();
manager.setupModelPathsSection();
manager.loadModelPaths();
document.querySelector('[data-model-paths-subtype="controlnet"]').checked = true;
await manager.handleModelPathsSubTypeToggles();
expect(manager.saveSetting).toHaveBeenCalledWith('enabled_other_sub_types', ['vae', 'controlnet']);
expect(document.getElementById('modelFolderPaths-controlnet')).not.toBeNull();
});
it('saves collected folder paths via saveSetting', async () => {
buildModalDom();
setStandaloneSettings();
const manager = createManager();
manager.saveSetting = vi.fn().mockResolvedValue();
manager.setupModelPathsSection();
manager.loadModelPaths();
// No rows exist until the user clicks Add
expect(document.querySelector('#modelFolderPaths-loras .extra-folder-path-input')).toBeNull();
manager.addModelFolderPathRow('loras');
document.querySelector('#modelFolderPaths-loras .extra-folder-path-input').value = '/data/loras';
await manager.updateModelFolderPaths('loras');
expect(manager.saveSetting).toHaveBeenCalledWith('folder_paths', {
loras: ['/data/loras'],
checkpoints: [],
unet: [],
embeddings: [],
});
expect(state.global.settings.folder_paths.loras).toEqual(['/data/loras']);
});
it('blocks saving when checkpoints and unet share a path', async () => {
buildModalDom();
setStandaloneSettings();
const manager = createManager();
manager.saveSetting = vi.fn().mockResolvedValue();
manager.setupModelPathsSection();
manager.loadModelPaths();
manager.addModelFolderPathRow('checkpoints');
manager.addModelFolderPathRow('unet');
document.querySelector('#modelFolderPaths-checkpoints .extra-folder-path-input').value = '/same/dir';
document.querySelector('#modelFolderPaths-unet .extra-folder-path-input').value = '/same/dir';
await manager.updateModelFolderPaths('checkpoints');
expect(manager.saveSetting).not.toHaveBeenCalled();
const ckptInput = document.querySelector('#modelFolderPaths-checkpoints .extra-folder-path-input');
expect(ckptInput.classList.contains('has-error')).toBe(true);
});
it('appends a trailing empty row after filling one, but not after a removal', async () => {
buildModalDom();
setStandaloneSettings({ folder_paths: { loras: ['/data/a'] } });
const manager = createManager();
manager.saveSetting = vi.fn().mockResolvedValue();
manager.setupModelPathsSection();
manager.loadModelPaths();
// Fill the trailing empty row -> save appends a fresh empty row
manager.addModelFolderPathRow('loras');
const rows = () => document.querySelectorAll('#modelFolderPaths-loras .extra-folder-path-row');
expect(rows()).toHaveLength(2);
rows()[1].querySelector('.extra-folder-path-input').value = '/data/b';
await manager.updateModelFolderPaths('loras');
expect(rows()).toHaveLength(3);
// Removing a row never resurrects an empty row
const removeBtn = rows()[0].querySelector('.remove-path-btn');
manager.removeModelFolderPathRow(removeBtn, 'loras');
await vi.waitFor(() => {
expect(manager.saveSetting).toHaveBeenCalledTimes(2);
});
// Two rows left: the saved '/data/b' plus the pre-existing trailing
// empty row — removal must not append yet another empty row.
expect(rows()).toHaveLength(2);
const emptyRows = Array.from(rows()).filter(
(row) => row.querySelector('.extra-folder-path-input').value === '',
);
expect(emptyRows).toHaveLength(1);
});
it('restores previous state when saving fails', async () => {
buildModalDom();
setStandaloneSettings({ folder_paths: { loras: ['/original'] } });
const manager = createManager();
manager.saveSetting = vi.fn().mockRejectedValue(new Error('nope'));
manager.setupModelPathsSection();
manager.loadModelPaths();
const input = document.querySelector('#modelFolderPaths-loras .extra-folder-path-input');
input.value = '/changed';
await manager.updateModelFolderPaths('loras');
expect(state.global.settings.folder_paths).toEqual({ loras: ['/original'] });
// Rows reloaded from restored state
const reloaded = document.querySelector('#modelFolderPaths-loras .extra-folder-path-input');
expect(reloaded.value).toBe('/original');
});
it('marks pending-restart cues after a successful save', async () => {
buildModalDom();
setStandaloneSettings();
const manager = createManager();
manager.saveSetting = vi.fn().mockResolvedValue();
manager.setupModelPathsSection();
manager.loadModelPaths();
const navItem = document.querySelector('.settings-nav-item[data-section="modelPaths"]');
expect(navItem.classList.contains('has-pending-restart')).toBe(false);
manager.addModelFolderPathRow('loras');
document.querySelector('#modelFolderPaths-loras .extra-folder-path-input').value = '/data/loras';
await manager.updateModelFolderPaths('loras');
expect(navItem.classList.contains('has-pending-restart')).toBe(true);
expect(document.getElementById('modelPathsRestartNotice').classList.contains('visible')).toBe(true);
// A unique-per-change banner id: dismissing it once must not mute
// future reminders (dismissed ids persist across restarts).
const restartBanners = Array.from(bannerService.banners.keys())
.filter((id) => id.startsWith('model-paths-restart-'));
expect(restartBanners).toHaveLength(1);
});
});
@@ -25,6 +25,7 @@ describe('Other Models disabled page', () => {
document.body.innerHTML = [ document.body.innerHTML = [
'<button id="enableOtherModelsBtn"></button>', '<button id="enableOtherModelsBtn"></button>',
'<button id="openOtherModelsSettingsBtn"></button>', '<button id="openOtherModelsSettingsBtn"></button>',
'<button id="openModelPathsSettingsBtn"></button>',
'<button id="openSettingsFolderBtn"></button>', '<button id="openSettingsFolderBtn"></button>',
].join(''); ].join('');
@@ -65,6 +66,27 @@ describe('Other Models disabled page', () => {
expect(showModal).toHaveBeenCalledWith('settingsModal'); expect(showModal).toHaveBeenCalledWith('settingsModal');
}); });
it('opens the Model Paths settings from the standalone no-folders state', async () => {
const showModal = vi.fn();
window.modalManager = { showModal };
const navItem = document.createElement('button');
navItem.className = 'settings-nav-item';
navItem.dataset.section = 'modelPaths';
const navClick = vi.fn();
navItem.addEventListener('click', navClick);
document.body.appendChild(navItem);
document.getElementById('openModelPathsSettingsBtn').dispatchEvent(
new MouseEvent('click', { bubbles: true }),
);
expect(showModal).toHaveBeenCalledWith('settingsModal');
await new Promise((resolve) => setTimeout(resolve, 150));
expect(navClick).toHaveBeenCalledTimes(1);
});
it('reveals the settings.json location from the standalone no-folders state', async () => { it('reveals the settings.json location from the standalone no-folders state', async () => {
global.fetch = vi.fn().mockResolvedValue({ global.fetch = vi.fn().mockResolvedValue({
ok: true, ok: true,
@@ -30,6 +30,7 @@
'language': 'en', 'language': 'en',
'llm_api_key_set': False, 'llm_api_key_set': False,
'other_models_paths_available': False, 'other_models_paths_available': False,
'standalone_mode': False,
'theme': 'dark', 'theme': 'dark',
}), }),
'success': True, 'success': True,
+131
View File
@@ -160,3 +160,134 @@ async def test_activate_library_unexpected_error_returns_500(monkeypatch):
assert response.status == 500 assert response.status == 500
assert payload["success"] is False assert payload["success"] is False
assert payload["error"] == "bad things" assert payload["error"] == "bad things"
class DummySettingsForGet:
def __init__(self, values=None):
self._values = dict(values or {})
self.settings_file = "/tmp/settings.json"
self.set_calls = []
def keys(self):
return self._values.keys()
def get(self, key, default=None):
return self._values.get(key, default)
def set(self, key, value):
self.set_calls.append((key, value))
self._values[key] = value
def get_startup_messages(self):
return []
def make_get_handler(values=None) -> SettingsHandler:
return SettingsHandler(
settings_service=DummySettingsForGet(values),
metadata_provider_updater=noop_async,
downloader_factory=dummy_downloader_factory,
)
@pytest.fixture
def patch_other_models_availability(monkeypatch):
monkeypatch.setattr(
config,
"get_other_models_availability",
lambda: {"available": False},
)
@pytest.mark.asyncio
async def test_get_settings_plugin_mode_hides_folder_paths(
monkeypatch, patch_other_models_availability
):
monkeypatch.delenv("LORA_MANAGER_STANDALONE", raising=False)
handler = make_get_handler(
{
"language": "en",
"folder_paths": {"loras": ["/models/loras"]},
}
)
response = await handler.get_settings(FakeRequest())
payload = json_payload(response)
assert response.status == 200
settings = payload["settings"]
assert settings["standalone_mode"] is False
assert "folder_paths" not in settings
assert "folder_path_schema" not in settings
@pytest.mark.asyncio
async def test_get_settings_standalone_exposes_folder_paths_and_schema(
monkeypatch, patch_other_models_availability
):
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
folder_paths = {"loras": ["/models/loras"], "vae": ["/models/vae"]}
handler = make_get_handler({"language": "en", "folder_paths": folder_paths})
response = await handler.get_settings(FakeRequest())
payload = json_payload(response)
assert response.status == 200
settings = payload["settings"]
assert settings["standalone_mode"] is True
assert settings["folder_paths"] == folder_paths
schema = settings["folder_path_schema"]
core_keys = [entry["key"] for entry in schema if entry["category"] == "core"]
assert core_keys == ["loras", "checkpoints", "unet", "embeddings"]
other_entries = {entry["key"]: entry for entry in schema if entry["category"] == "other"}
assert other_entries["vae"]["sub_type"] == "vae"
assert other_entries["text_encoders"]["sub_type"] == "text_encoder"
@pytest.mark.asyncio
async def test_update_settings_passes_folder_paths_through(
monkeypatch, patch_other_models_availability
):
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
handler = make_get_handler({"folder_paths": {}})
new_paths = {"loras": ["/models/loras"]}
response = await handler.update_settings(
FakeRequest(json_data={"folder_paths": new_paths})
)
payload = json_payload(response)
assert response.status == 200
assert payload["success"] is True
assert handler._settings.set_calls == [("folder_paths", new_paths)]
@pytest.mark.asyncio
async def test_get_settings_standalone_filters_template_placeholders(
monkeypatch, patch_other_models_availability
):
"""Fresh installs are seeded from settings.json.example; its placeholder
paths must not show up as real values in the Model Paths UI."""
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
handler = make_get_handler(
{
"folder_paths": {
"loras": ["C:/path/to/your/loras_folder", "/real/loras"],
"vae": ["C:/path/to/another/vae_folder"],
}
}
)
handler._settings.get_template_folder_path_placeholders = lambda: {
"C:/path/to/your/loras_folder",
"C:/path/to/another/vae_folder",
}
response = await handler.get_settings(FakeRequest())
payload = json_payload(response)
assert response.status == 200
assert payload["settings"]["folder_paths"] == {
"loras": ["/real/loras"],
"vae": [],
}
+17 -1
View File
@@ -52,12 +52,18 @@ def test_missing_settings_creates_defaults_and_emits_warnings(tmp_path):
actions = warning.get("actions") or [] actions = warning.get("actions") or []
assert actions == [ assert actions == [
{
"action": "open-model-paths-settings",
"label": "Configure model folders",
"type": "primary",
"icon": "fas fa-cog",
},
{ {
"action": "open-settings-location", "action": "open-settings-location",
"label": "Open settings folder", "label": "Open settings folder",
"type": "primary", "type": "primary",
"icon": "fas fa-folder-open", "icon": "fas fa-folder-open",
} },
] ]
@@ -155,3 +161,13 @@ def test_apply_settings_dir_from_argv():
os.environ.pop("LORA_MANAGER_SETTINGS_DIR", None) os.environ.pop("LORA_MANAGER_SETTINGS_DIR", None)
else: else:
os.environ["LORA_MANAGER_SETTINGS_DIR"] = previous os.environ["LORA_MANAGER_SETTINGS_DIR"] = previous
def test_template_folder_path_placeholders_are_exposed():
manager = get_settings_manager()
placeholders = manager.get_template_folder_path_placeholders()
assert "C:/path/to/your/loras_folder" in placeholders
assert "C:/path/to/another/embeddings_folder" in placeholders
assert len(placeholders) == 8
+33 -1
View File
@@ -42,4 +42,36 @@ class TestIsEmptyPlaceholderHash:
def test_rejects_non_strings(self): def test_rejects_non_strings(self):
assert not is_empty_placeholder_hash(None) assert not is_empty_placeholder_hash(None)
assert not is_empty_placeholder_hash(123) assert not is_empty_placeholder_hash(123)
class TestFolderPathSchema:
def test_core_keys_first_in_canonical_order(self):
from py.utils.constants import CORE_FOLDER_PATH_KEYS, folder_path_schema
schema = folder_path_schema()
core = [entry for entry in schema if entry["category"] == "core"]
assert [entry["key"] for entry in core] == CORE_FOLDER_PATH_KEYS
assert all(entry["sub_type"] is None for entry in core)
assert schema[: len(core)] == core
def test_other_entries_derive_from_subtypes_table(self):
from py.utils.constants import OTHER_MODEL_FOLDER_SUBTYPES, folder_path_schema
schema = folder_path_schema()
other = {entry["key"]: entry for entry in schema if entry["category"] == "other"}
assert set(other) == set(OTHER_MODEL_FOLDER_SUBTYPES)
for folder_key, sub_type in OTHER_MODEL_FOLDER_SUBTYPES.items():
assert other[folder_key]["sub_type"] == sub_type
def test_text_encoder_exposes_both_folder_keys(self):
from py.utils.constants import folder_path_schema
text_encoder_keys = [
entry["key"]
for entry in folder_path_schema()
if entry["sub_type"] == "text_encoder"
]
assert text_encoder_keys == ["text_encoders", "clip"]