diff --git a/locales/de.json b/locales/de.json index d0873fac..a428c70d 100644 --- a/locales/de.json +++ b/locales/de.json @@ -505,7 +505,9 @@ "saveSuccess": "Zusätzliche Ordnerpfade aktualisiert. Neustart erforderlich, um Änderungen anzuwenden.", "saveError": "Fehler beim Aktualisieren der zusätzlichen Ordnerpfade: {message}", "validation": { - "duplicatePath": "Dieser Pfad ist bereits konfiguriert" + "duplicatePath": "Dieser Pfad ist bereits konfiguriert", + "checkpointUnetOverlap": "Derselbe Pfad kann nicht für Checkpoints und Diffusionsmodelle verwendet werden: {paths}", + "checkpointUnetOverlapInline": "Dieser Pfad wird bereits für einen anderen Modelltyp verwendet. Bitte verwenden Sie separate Ordner für Checkpoints und Diffusionsmodelle." } }, "priorityTags": { diff --git a/locales/en.json b/locales/en.json index 15882d4d..df1526f6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -505,7 +505,9 @@ "saveSuccess": "Extra folder paths updated. Restart required to apply changes.", "saveError": "Failed to update extra folder paths: {message}", "validation": { - "duplicatePath": "This path is already configured" + "duplicatePath": "This path is already configured", + "checkpointUnetOverlap": "Cannot use the same path for both checkpoints and diffusion models: {paths}", + "checkpointUnetOverlapInline": "This path is also used for a different model type. Use separate folders for checkpoints and diffusion models." } }, "priorityTags": { diff --git a/locales/es.json b/locales/es.json index c3f6628e..57ff5506 100644 --- a/locales/es.json +++ b/locales/es.json @@ -505,7 +505,9 @@ "saveSuccess": "Rutas de carpetas adicionales actualizadas. Se requiere reinicio para aplicar los cambios.", "saveError": "Error al actualizar las rutas de carpetas adicionales: {message}", "validation": { - "duplicatePath": "Esta ruta ya está configurada" + "duplicatePath": "Esta ruta ya está configurada", + "checkpointUnetOverlap": "No se puede usar la misma ruta para checkpoints y modelos de difusión: {paths}", + "checkpointUnetOverlapInline": "Esta ruta ya se usa para otro tipo de modelo. Use carpetas separadas para checkpoints y modelos de difusión." } }, "priorityTags": { diff --git a/locales/fr.json b/locales/fr.json index 7d695684..8bbcd691 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -505,7 +505,9 @@ "saveSuccess": "Chemins de dossiers supplémentaires mis à jour. Redémarrage requis pour appliquer les changements.", "saveError": "Échec de la mise à jour des chemins de dossiers supplémentaires: {message}", "validation": { - "duplicatePath": "Ce chemin est déjà configuré" + "duplicatePath": "Ce chemin est déjà configuré", + "checkpointUnetOverlap": "Impossible d'utiliser le même chemin pour les checkpoints et les modèles de diffusion : {paths}", + "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." } }, "priorityTags": { diff --git a/locales/he.json b/locales/he.json index d448a1fe..6903c380 100644 --- a/locales/he.json +++ b/locales/he.json @@ -505,7 +505,9 @@ "saveSuccess": "נתיבי תיקיות נוספים עודכנו. נדרשת הפעלה מחדש כדי להחיל את השינויים.", "saveError": "נכשל בעדכון נתיבי תיקיות נוספים: {message}", "validation": { - "duplicatePath": "נתיב זה כבר מוגדר" + "duplicatePath": "נתיב זה כבר מוגדר", + "checkpointUnetOverlap": "לא ניתן להשתמש באותו נתיב עבור checkpoints ומודלי דיפוזיה: {paths}", + "checkpointUnetOverlapInline": "הנתיב הזה כבר נמצא בשימוש עבור סוג מודל אחר. יש להשתמש בתיקיות נפרדות עבור checkpoints ומודלי דיפוזיה." } }, "priorityTags": { diff --git a/locales/ja.json b/locales/ja.json index 76978e38..a3930b18 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -505,7 +505,9 @@ "saveSuccess": "追加フォルダーパスを更新しました。変更を適用するには再起動が必要です。", "saveError": "追加フォルダーパスの更新に失敗しました: {message}", "validation": { - "duplicatePath": "このパスはすでに設定されています" + "duplicatePath": "このパスはすでに設定されています", + "checkpointUnetOverlap": "checkpoints と diffusion models に同じパスは使用できません:{paths}", + "checkpointUnetOverlapInline": "このパスは別のモデルタイプですでに使用されています。checkpoints と diffusion models には別々のフォルダを使用してください。" } }, "priorityTags": { diff --git a/locales/ko.json b/locales/ko.json index 5aedd529..ded25df2 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -505,7 +505,9 @@ "saveSuccess": "추가 폴다 경로가 업데이트되었습니다. 변경 사항을 적용하려면 재시작이 필요합니다.", "saveError": "추가 폴다 경로 업데이트 실패: {message}", "validation": { - "duplicatePath": "이 경로는 이미 구성되어 있습니다" + "duplicatePath": "이 경로는 이미 구성되어 있습니다", + "checkpointUnetOverlap": "checkpoints와 diffusion models에 동일한 경로를 사용할 수 없습니다: {paths}", + "checkpointUnetOverlapInline": "이 경로는 다른 모델 유형에 이미 사용 중입니다. checkpoints와 diffusion models에 별도의 폴더를 사용하세요." } }, "priorityTags": { diff --git a/locales/ru.json b/locales/ru.json index f1788e7b..1dc9050a 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -505,7 +505,9 @@ "saveSuccess": "Дополнительные пути к папкам обновлены. Требуется перезапуск для применения изменений.", "saveError": "Не удалось обновить дополнительные пути к папкам: {message}", "validation": { - "duplicatePath": "Этот путь уже настроен" + "duplicatePath": "Этот путь уже настроен", + "checkpointUnetOverlap": "Нельзя использовать один и тот же путь для checkpoints и diffusion models: {paths}", + "checkpointUnetOverlapInline": "Этот путь уже используется для другого типа модели. Используйте отдельные папки для checkpoints и diffusion models." } }, "priorityTags": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 91679dfd..c7385b71 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -505,7 +505,9 @@ "saveSuccess": "额外文件夹路径已更新,需要重启才能生效。", "saveError": "更新额外文件夹路径失败:{message}", "validation": { - "duplicatePath": "此路径已配置" + "duplicatePath": "此路径已配置", + "checkpointUnetOverlap": "checkpoints 和 diffusion models 不能使用相同的路径:{paths}", + "checkpointUnetOverlapInline": "此路径已被用于另一种模型类型。请为 checkpoints 和 diffusion models 使用不同的文件夹。" } }, "priorityTags": { diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 3b1de91c..f3828223 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -505,7 +505,9 @@ "saveSuccess": "額外資料夾路徑已更新,需要重啟才能生效。", "saveError": "更新額外資料夾路徑失敗:{message}", "validation": { - "duplicatePath": "此路徑已設定" + "duplicatePath": "此路徑已設定", + "checkpointUnetOverlap": "checkpoints 和 diffusion models 不能使用相同的路徑:{paths}", + "checkpointUnetOverlapInline": "此路徑已被用於另一種模型類型。請為 checkpoints 和 diffusion models 使用不同的資料夾。" } }, "priorityTags": { diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 0a5fe587..f990b7c3 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -630,12 +630,37 @@ class SettingsManager: return False + @staticmethod + def _normalize_path_set(paths: Iterable[str]) -> set[str]: + """Normalize an iterable of paths for set-based overlap comparison. + + Resolves symlinks via ``os.path.realpath`` when the path exists on disk, + then applies ``os.path.normcase`` + ``os.path.normpath`` for consistent + cross-platform comparison. Non-string / empty entries are skipped. + """ + result: set[str] = set() + for p in paths: + if not isinstance(p, str): + continue + stripped = p.strip() + if not stripped: + continue + if os.path.exists(stripped): + stripped = os.path.normpath(os.path.realpath(stripped)) + result.add(os.path.normcase(stripped)) + return result + def _validate_folder_paths( self, library_name: str, folder_paths: Mapping[str, Iterable[str]], ) -> None: - """Ensure folder paths do not overlap with other libraries.""" + """Ensure folder paths do not overlap with other libraries. + + Also detects checkpoints ↔ unet path overlap within the same library + (including via symlink resolution), which is a configuration error since + these model types must use separate physical folders. + """ libraries = self.settings.get("libraries", {}) normalized_new: Dict[str, Dict[str, str]] = {} for key, values in folder_paths.items(): @@ -673,6 +698,22 @@ class SettingsManager: f"Folder path(s) {collisions} already assigned to library '{other_name}'" ) + # Checkpoints ↔ unet overlap within the same library + ckpt_paths = folder_paths.get("checkpoints", []) or [] + unet_paths = folder_paths.get("unet", []) or [] + if ckpt_paths and unet_paths: + ckpt_real = self._normalize_path_set(ckpt_paths) + unet_real = self._normalize_path_set(unet_paths) + overlap = ckpt_real & unet_real + if overlap: + collisions = ", ".join(sorted(overlap)) + raise ValueError( + f"Path(s) {collisions} are configured for both " + f"'checkpoints' and 'unet' (diffusion models). " + f"These model types must use separate physical folders. " + f"Please remove one of the conflicting entries." + ) + def _update_active_library_entry( self, *, @@ -1547,8 +1588,12 @@ class SettingsManager: portable_switch_pending = True self._prepare_portable_switch(value) if key == "folder_paths" and isinstance(value, Mapping): + active_name = self.get_active_library_name() + self._validate_folder_paths(active_name, value) self._update_active_library_entry(folder_paths=value) # type: ignore[arg-type] elif key == "extra_folder_paths" and isinstance(value, Mapping): + active_name = self.get_active_library_name() + self._validate_folder_paths(active_name, value) self._update_active_library_entry(extra_folder_paths=value) # type: ignore[arg-type] elif key == "default_lora_root": self._update_active_library_entry(default_lora_root=str(value)) diff --git a/static/css/components/modal/settings-modal.css b/static/css/components/modal/settings-modal.css index 0f4339ff..3ca0be7f 100644 --- a/static/css/components/modal/settings-modal.css +++ b/static/css/components/modal/settings-modal.css @@ -1562,6 +1562,29 @@ input:checked + .toggle-slider:before { box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1); } +.extra-folder-path-row .path-controls .extra-folder-path-input.has-error { + border-color: var(--lora-error); + background-color: rgba(220, 53, 69, 0.08); + background-color: rgba(from var(--lora-error) r g b / 0.08); +} + +.extra-folder-path-row .path-controls .extra-folder-path-input.has-error:focus { + box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.15); + box-shadow: 0 0 0 2px rgba(from var(--lora-error) r g b / 0.15); +} + +.extra-folder-path-error { + color: var(--lora-error); + font-size: 0.8em; + margin-top: 4px; + line-height: 1.4; + display: none; +} + +.extra-folder-path-error.visible { + display: block; +} + .extra-folder-path-row .path-controls .remove-path-btn { width: 32px; height: 32px; diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index a143a20f..ee8ddf57 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -1693,13 +1693,15 @@ export class SettingsManager { +
`; container.appendChild(row); @@ -1713,7 +1715,63 @@ export class SettingsManager { } } + clearExtraFolderPathError(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 = ''; + } + } + } + + _clearAllExtraFolderPathErrors() { + document.querySelectorAll('.extra-folder-path-input.has-error').forEach((input) => { + input.classList.remove('has-error'); + }); + document.querySelectorAll('.extra-folder-path-error.visible').forEach((el) => { + el.classList.remove('visible'); + el.textContent = ''; + }); + } + + _markExtraFolderPathsError(modelType, overlappingPaths, showMessage = false) { + const container = document.getElementById(`extraFolderPaths-${modelType}`); + 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'); + } + } + } + } + }); + } + + removeExtraFolderPathRow(btn, modelType) { + const row = btn.closest('.extra-folder-path-row'); + if (row) { + row.remove(); + this.updateExtraFolderPaths(modelType); + } + } + async updateExtraFolderPaths(changedModelType) { + // Clear previous errors + this._clearAllExtraFolderPathErrors(); + const extraFolderPaths = {}; // Collect paths for all model types @@ -1734,6 +1792,32 @@ export class SettingsManager { extraFolderPaths[modelType] = paths; }); + // Client-side pre-check: checkpoints and unet must not share the same path. + // Normalise paths to reduce false negatives vs the backend's realpath + normcase. + const normalise = (p) => p.replace(/[/\\]+$/, '').toLowerCase(); + const ckptSet = new Set((extraFolderPaths.checkpoints || []).map(normalise)); + const unetSet = new Set((extraFolderPaths.unet || []).map(normalise)); + const ckptOverlap = (extraFolderPaths.checkpoints || []).filter(p => p && unetSet.has(normalise(p))); + const unetOverlap = (extraFolderPaths.unet || []).filter(p => p && ckptSet.has(normalise(p))); + const hasOverlap = ckptOverlap.length > 0 || unetOverlap.length > 0; + + if (hasOverlap) { + // Error message only on the side the user just edited. + // The other side gets red border only (passive conflict indicator). + if (changedModelType === 'checkpoints') { + this._markExtraFolderPathsError('checkpoints', ckptOverlap, true); + this._markExtraFolderPathsError('unet', unetOverlap, false); + } else if (changedModelType === 'unet') { + this._markExtraFolderPathsError('unet', unetOverlap, true); + this._markExtraFolderPathsError('checkpoints', ckptOverlap, false); + } else { + // Pre-existing conflict from direct config edit — mark both without messages + this._markExtraFolderPathsError('checkpoints', ckptOverlap, false); + this._markExtraFolderPathsError('unet', unetOverlap, false); + } + return; + } + // Check if paths have actually changed const currentPaths = state.global.settings.extra_folder_paths || {}; const pathsChanged = JSON.stringify(currentPaths) !== JSON.stringify(extraFolderPaths);