diff --git a/locales/de.json b/locales/de.json index 231dc3bb..65dde105 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Wählen Sie ein Stammverzeichnis", "selectModelRoot": "Modell-Stammverzeichnis auswählen:", "selectTypeRoot": "{type}-Stammverzeichnis auswählen:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Zielordnerpfad:", "browseFolders": "Ordner durchsuchen:", "createNewFolder": "Neuen Ordner erstellen", diff --git a/locales/en.json b/locales/en.json index e28de79d..9b83bf13 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Select a root directory", "selectModelRoot": "Select Model Root:", "selectTypeRoot": "Select {type} Root:", + "routingOverride": { + "label": "Destination type:", + "tooltip": "Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Target Folder Path:", "browseFolders": "Browse Folders:", "createNewFolder": "Create new folder", diff --git a/locales/es.json b/locales/es.json index a4af9c81..20ba0a9d 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Selecciona un directorio raíz", "selectModelRoot": "Seleccionar raíz del modelo:", "selectTypeRoot": "Seleccionar raíz de {type}:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Ruta de carpeta de destino:", "browseFolders": "Explorar carpetas:", "createNewFolder": "Crear nueva carpeta", diff --git a/locales/fr.json b/locales/fr.json index c87f7bbd..cbf4e1d2 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Sélectionner un répertoire racine", "selectModelRoot": "Sélectionner la racine du modèle :", "selectTypeRoot": "Sélectionner la racine {type} :", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Chemin du dossier cible :", "browseFolders": "Parcourir les dossiers :", "createNewFolder": "Créer un nouveau dossier", diff --git a/locales/he.json b/locales/he.json index f6c9915e..88dfbe9a 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "בחר ספריית שורש", "selectModelRoot": "בחר שורש מודל:", "selectTypeRoot": "בחר שורש {type}:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "נתיב תיקיית יעד:", "browseFolders": "דפדף בתיקיות:", "createNewFolder": "צור תיקייה חדשה", diff --git a/locales/ja.json b/locales/ja.json index 47372056..3314c791 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "ルートディレクトリを選択", "selectModelRoot": "モデルルートを選択:", "selectTypeRoot": "{type}ルートを選択:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "ターゲットフォルダパス:", "browseFolders": "フォルダを参照:", "createNewFolder": "新しいフォルダを作成", diff --git a/locales/ko.json b/locales/ko.json index dfe20317..b56aedd1 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "루트 디렉토리를 선택하세요", "selectModelRoot": "모델 루트 선택:", "selectTypeRoot": "{type} 루트 선택:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "대상 폴더 경로:", "browseFolders": "폴더 탐색:", "createNewFolder": "새 폴더 만들기", diff --git a/locales/ru.json b/locales/ru.json index 1c92c656..93f92f7b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Выберите корневую папку", "selectModelRoot": "Выберите корень моделей:", "selectTypeRoot": "Выберите корень {type}:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Путь к целевой папке:", "browseFolders": "Обзор папок:", "createNewFolder": "Создать новую папку", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 9a5013b4..b8db4a3a 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "选择根目录", "selectModelRoot": "选择模型根目录:", "selectTypeRoot": "选择 {type} 根目录:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "目标文件夹路径:", "browseFolders": "浏览文件夹:", "createNewFolder": "新建文件夹", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index f0d5f544..d8216de7 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "選擇根目錄", "selectModelRoot": "選擇模型根目錄:", "selectTypeRoot": "選擇 {type} 根目錄:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "目標資料夾路徑:", "browseFolders": "瀏覽資料夾:", "createNewFolder": "建立新資料夾", diff --git a/static/css/components/modal/download-modal.css b/static/css/components/modal/download-modal.css index 63386a5b..1ea968ff 100644 --- a/static/css/components/modal/download-modal.css +++ b/static/css/components/modal/download-modal.css @@ -1088,3 +1088,56 @@ [data-theme="dark"] .batch-preview-group-body { background: var(--border-color); } + + +/* Routing override segmented toggle (Checkpoint / Diffusion Model) shown on + the checkpoints page of the download modal when auto-routing can misdetect. */ +.routing-override-group { + margin-bottom: var(--space-2); +} + +.routing-override-group label { + display: block; + margin-bottom: 8px; + color: var(--text-color); +} + +.routing-override-toggle { + display: flex; + gap: 0; +} + +.routing-override-option { + flex: 1; + padding: 6px 8px; + border: 1px solid var(--border-color); + background: var(--bg-color); + color: var(--text-color); + opacity: 0.75; + cursor: pointer; + transition: var(--transition-base); + font-size: 0.9em; +} + +.routing-override-option:first-child { + border-radius: var(--border-radius-xs) 0 0 var(--border-radius-xs); + border-right: none; +} + +.routing-override-option:last-child { + border-radius: 0 var(--border-radius-xs) var(--border-radius-xs) 0; +} + +.routing-override-option:not(.active):hover, +.routing-override-option:not(.active):focus-visible { + border-color: var(--lora-accent); + opacity: 1; +} + +.routing-override-option.active { + background: var(--lora-accent); + color: white; + border-color: var(--lora-accent); + opacity: 1; + cursor: default; +} diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index ff4a8e23..7520a958 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -34,6 +34,9 @@ export class DownloadManager { this.selectedFolder = ''; this.apiClient = null; this.useDefaultPath = false; + // Manual checkpoint/diffusion root override on the location step: + // null follows the auto classification, true/false is user-picked. + this._routingOverride = null; // Multi-file selection state: selectedFile stays the first selected // file for backward compatibility with single-file flows (#1058). @@ -127,6 +130,11 @@ export class DownloadManager { // Default path toggle handler document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath); + // Checkpoint/diffusion routing override toggle (checkpoints page only) + document.querySelectorAll('#routingOverrideGroup .routing-override-option').forEach(btn => { + btn.addEventListener('click', () => this.handleRoutingOverrideChange(btn.dataset.routing === 'diffusion')); + }); + // Auto-append newline after pasting a URL so users can paste multiple URLs in succession setupAutoNewlineOnPaste('modelUrl'); } @@ -180,6 +188,7 @@ export class DownloadManager { this.selectedFiles = []; this._lastDownloadError = null; this._isDiffusionModel = false; + this._routingOverride = null; this.selectedFolder = ''; this.batchModels = []; @@ -974,48 +983,12 @@ export class DownloadManager { try { this._isDiffusionModel = await this._resolveIsDiffusionModel(); this._otherSubType = await this._resolveOtherSubType(); + // Each location-step entry starts from the auto classification; + // the user can override it via the checkpoint/diffusion toggle. + this._routingOverride = null; + this._syncRoutingOverrideToggle(); - let rootsData; - if (this._isDiffusionModel && this.apiClient.modelType === 'checkpoints') { - rootsData = await this.apiClient.fetchModelRoots('diffusion_model'); - } else if (this.apiClient.modelType === 'other' && this._otherSubType) { - rootsData = await this.apiClient.fetchModelRoots(this._otherSubType); - } else { - // An undecidable other sub_type (null) intentionally lands - // here: fetchModelRoots() lists all other roots so the user - // can pick manually. - rootsData = await this.apiClient.fetchModelRoots(); - } - const modelRoot = document.getElementById('modelRoot'); - modelRoot.innerHTML = rootsData.roots.map(root => - `` - ).join(''); - - let defaultRoot; - let subtypeDisplay; - if (this.apiClient.modelType === 'other') { - const otherDefaultRoots = state.global.settings.default_other_roots || {}; - defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : ''; - subtypeDisplay = this._otherSubType - ? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType) - : this.apiClient.apiConfig.config.displayName; - } else { - const singularType = this._isDiffusionModel - ? 'unet' - : this.apiClient.modelType.replace(/s$/, ''); - const defaultRootKey = `default_${singularType}_root`; - defaultRoot = state.global.settings[defaultRootKey]; - subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName; - } - console.log('Default root:', defaultRoot); - console.log('Available roots:', rootsData.roots); - if (defaultRoot && rootsData.roots.includes(defaultRoot)) { - console.log(`Setting default root: ${defaultRoot}`); - modelRoot.value = defaultRoot; - } - - document.getElementById('modelRootLabel').textContent = - translate('modals.download.selectTypeRoot', { type: subtypeDisplay }); + await this._populateModelRoots(); // Set autocomplete="off" on folderPath input const folderPathInput = document.getElementById('folderPath'); @@ -1035,6 +1008,7 @@ export class DownloadManager { }); // Setup model root change handler + const modelRoot = document.getElementById('modelRoot'); modelRoot.addEventListener('change', async () => { await this.initializeFolderTree(); this.updateTargetPath(); @@ -1049,6 +1023,116 @@ export class DownloadManager { } } + /** + * Effective checkpoint/diffusion routing for the location step: the + * user's manual override when set, otherwise the auto classification. + */ + _getEffectiveIsDiffusionModel() { + return this._routingOverride ?? this._isDiffusionModel; + } + + /** + * Fetch the root list for the effective routing group and repopulate + * the root dropdown, preselecting the matching default root and + * updating the root label. + */ + async _populateModelRoots() { + const isDiffusion = this._getEffectiveIsDiffusionModel(); + + let rootsData; + if (isDiffusion && this.apiClient.modelType === 'checkpoints') { + rootsData = await this.apiClient.fetchModelRoots('diffusion_model'); + } else if (this.apiClient.modelType === 'other' && this._otherSubType) { + rootsData = await this.apiClient.fetchModelRoots(this._otherSubType); + } else { + // An undecidable other sub_type (null) intentionally lands + // here: fetchModelRoots() lists all other roots so the user + // can pick manually. + rootsData = await this.apiClient.fetchModelRoots(); + } + const modelRoot = document.getElementById('modelRoot'); + modelRoot.innerHTML = rootsData.roots.map(root => + `` + ).join(''); + + let defaultRoot; + let subtypeDisplay; + if (this.apiClient.modelType === 'other') { + const otherDefaultRoots = state.global.settings.default_other_roots || {}; + defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : ''; + subtypeDisplay = this._otherSubType + ? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType) + : this.apiClient.apiConfig.config.displayName; + } else { + const singularType = isDiffusion + ? 'unet' + : this.apiClient.modelType.replace(/s$/, ''); + const defaultRootKey = `default_${singularType}_root`; + defaultRoot = state.global.settings[defaultRootKey]; + subtypeDisplay = isDiffusion ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName; + } + console.log('Default root:', defaultRoot); + console.log('Available roots:', rootsData.roots); + if (defaultRoot && rootsData.roots.includes(defaultRoot)) { + console.log(`Setting default root: ${defaultRoot}`); + modelRoot.value = defaultRoot; + } + + document.getElementById('modelRootLabel').textContent = + translate('modals.download.selectTypeRoot', { type: subtypeDisplay }); + } + + /** + * Show the checkpoint/diffusion override toggle on the checkpoints page + * (hidden elsewhere) and reflect the effective routing on its options. + */ + _syncRoutingOverrideToggle() { + const group = document.getElementById('routingOverrideGroup'); + if (!group) return; + const isCheckpoints = this.apiClient.modelType === 'checkpoints'; + group.style.display = isCheckpoints ? '' : 'none'; + if (!isCheckpoints) return; + + const effective = this._getEffectiveIsDiffusionModel(); + group.querySelectorAll('.routing-override-option').forEach(btn => { + const active = (btn.dataset.routing === 'diffusion') === effective; + btn.classList.toggle('active', active); + btn.setAttribute('aria-pressed', active ? 'true' : 'false'); + }); + } + + /** + * Handle the user switching the checkpoint/diffusion routing toggle. + * The override must never be silently ignored: the backend re-derives + * the routing group only under "use default path", so overriding turns + * that off for this session (the localStorage preference is left + * untouched) to guarantee the picked root is the actual destination. + */ + async handleRoutingOverrideChange(isDiffusion) { + if (this.apiClient.modelType !== 'checkpoints') return; + + const overridden = isDiffusion !== this._isDiffusionModel; + this._routingOverride = overridden ? isDiffusion : null; + + if (overridden && this.useDefaultPath) { + this.useDefaultPath = false; + const toggleInput = document.getElementById('useDefaultPath'); + if (toggleInput) { + toggleInput.checked = false; + } + } + + this._syncRoutingOverrideToggle(); + + try { + await this._populateModelRoots(); + await this.initializeFolderTree(); + this.updatePathSelectionUI(); + } catch (error) { + showToast('toast.downloads.loadError', { message: error.message }, 'error'); + } + } + /** * Decide whether this download routes to the diffusion model (unet) * roots rather than the checkpoint roots. The backend owns the routing @@ -2554,13 +2638,13 @@ export class DownloadManager { const modelRoot = document.getElementById('modelRoot').value; const config = this.apiClient.apiConfig.config; - const subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : config.displayName; + const subtypeDisplay = this._getEffectiveIsDiffusionModel() ? 'Diffusion Model' : config.displayName; let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: subtypeDisplay }); if (modelRoot) { if (this.useDefaultPath) { try { - const singularType = this._isDiffusionModel + const singularType = this._getEffectiveIsDiffusionModel() ? 'unet' : this.apiClient.modelType.replace(/s$/, ''); const templates = state.global?.settings?.download_path_templates; diff --git a/templates/components/modals/download_modal.html b/templates/components/modals/download_modal.html index f92f3230..45c2073c 100644 --- a/templates/components/modals/download_modal.html +++ b/templates/components/modals/download_modal.html @@ -80,6 +80,14 @@ +