mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-11 00:10:14 -03:00
Compare commits
11 Commits
6ca411e4e4
...
v1.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf6a614e0d | ||
|
|
feab01cd9c | ||
|
|
966024e534 | ||
|
|
2018722cc8 | ||
|
|
9d85c2a44a | ||
|
|
03dd047e62 | ||
|
|
86b547c1e0 | ||
|
|
bab9752c8b | ||
|
|
774cc1be86 | ||
|
|
234b73c8a2 | ||
|
|
abd06c48f4 |
File diff suppressed because it is too large
Load Diff
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "Zusätzliche Ordnerpfade aktualisiert. Neustart erforderlich, um Änderungen anzuwenden.",
|
"saveSuccess": "Zusätzliche Ordnerpfade aktualisiert. Neustart erforderlich, um Änderungen anzuwenden.",
|
||||||
"saveError": "Fehler beim Aktualisieren der zusätzlichen Ordnerpfade: {message}",
|
"saveError": "Fehler beim Aktualisieren der zusätzlichen Ordnerpfade: {message}",
|
||||||
"validation": {
|
"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": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "Download wird vorbereitet...",
|
"preparing": "Download wird vorbereitet...",
|
||||||
"downloadedPreview": "Vorschaubild heruntergeladen",
|
"downloadedPreview": "Vorschaubild heruntergeladen",
|
||||||
"downloadingFile": "{type}-Datei wird heruntergeladen",
|
"downloadingFile": "{type}-Datei wird heruntergeladen",
|
||||||
"finalizing": "Download wird abgeschlossen..."
|
"finalizing": "Download wird abgeschlossen...",
|
||||||
|
"cancelling": "Download wird abgebrochen...",
|
||||||
|
"cancelled": "Download abgebrochen"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "Aktuelle Datei:",
|
"currentFile": "Aktuelle Datei:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "Beispielbilder {action} abgeschlossen",
|
"imagesCompleted": "Beispielbilder {action} abgeschlossen",
|
||||||
"imagesFailed": "Beispielbilder {action} fehlgeschlagen",
|
"imagesFailed": "Beispielbilder {action} fehlgeschlagen",
|
||||||
"loadError": "Fehler beim Laden der Downloads: {message}",
|
"loadError": "Fehler beim Laden der Downloads: {message}",
|
||||||
"downloadError": "Download-Fehler: {message}"
|
"downloadError": "Download-Fehler: {message}",
|
||||||
|
"downloadStopped": "Download abgebrochen"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "Fehler beim Laden des Ordnerbaums",
|
"folderTreeFailed": "Fehler beim Laden des Ordnerbaums",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "Extra folder paths updated. Restart required to apply changes.",
|
"saveSuccess": "Extra folder paths updated. Restart required to apply changes.",
|
||||||
"saveError": "Failed to update extra folder paths: {message}",
|
"saveError": "Failed to update extra folder paths: {message}",
|
||||||
"validation": {
|
"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": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "Preparing download...",
|
"preparing": "Preparing download...",
|
||||||
"downloadedPreview": "Downloaded preview image",
|
"downloadedPreview": "Downloaded preview image",
|
||||||
"downloadingFile": "Downloading {type} file",
|
"downloadingFile": "Downloading {type} file",
|
||||||
"finalizing": "Finalizing download..."
|
"finalizing": "Finalizing download...",
|
||||||
|
"cancelling": "Cancelling download...",
|
||||||
|
"cancelled": "Download cancelled"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "Current file:",
|
"currentFile": "Current file:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "Example images {action} completed",
|
"imagesCompleted": "Example images {action} completed",
|
||||||
"imagesFailed": "Example images {action} failed",
|
"imagesFailed": "Example images {action} failed",
|
||||||
"loadError": "Error loading downloads: {message}",
|
"loadError": "Error loading downloads: {message}",
|
||||||
"downloadError": "Download error: {message}"
|
"downloadError": "Download error: {message}",
|
||||||
|
"downloadStopped": "Download cancelled"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "Failed to load folder tree",
|
"folderTreeFailed": "Failed to load folder tree",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "Rutas de carpetas adicionales actualizadas. Se requiere reinicio para aplicar los cambios.",
|
"saveSuccess": "Rutas de carpetas adicionales actualizadas. Se requiere reinicio para aplicar los cambios.",
|
||||||
"saveError": "Error al actualizar las rutas de carpetas adicionales: {message}",
|
"saveError": "Error al actualizar las rutas de carpetas adicionales: {message}",
|
||||||
"validation": {
|
"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": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "Preparando descarga...",
|
"preparing": "Preparando descarga...",
|
||||||
"downloadedPreview": "Imagen de vista previa descargada",
|
"downloadedPreview": "Imagen de vista previa descargada",
|
||||||
"downloadingFile": "Descargando archivo de {type}",
|
"downloadingFile": "Descargando archivo de {type}",
|
||||||
"finalizing": "Finalizando descarga..."
|
"finalizing": "Finalizando descarga...",
|
||||||
|
"cancelling": "Cancelando descarga...",
|
||||||
|
"cancelled": "Descarga cancelada"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "Archivo actual:",
|
"currentFile": "Archivo actual:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "Imágenes de ejemplo {action} completadas",
|
"imagesCompleted": "Imágenes de ejemplo {action} completadas",
|
||||||
"imagesFailed": "Imágenes de ejemplo {action} fallidas",
|
"imagesFailed": "Imágenes de ejemplo {action} fallidas",
|
||||||
"loadError": "Error al cargar descargas: {message}",
|
"loadError": "Error al cargar descargas: {message}",
|
||||||
"downloadError": "Error de descarga: {message}"
|
"downloadError": "Error de descarga: {message}",
|
||||||
|
"downloadStopped": "Descarga cancelada"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "Error al cargar árbol de carpetas",
|
"folderTreeFailed": "Error al cargar árbol de carpetas",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "Chemins de dossiers supplémentaires mis à jour. Redémarrage requis pour appliquer les changements.",
|
"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}",
|
"saveError": "Échec de la mise à jour des chemins de dossiers supplémentaires: {message}",
|
||||||
"validation": {
|
"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": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "Préparation du téléchargement...",
|
"preparing": "Préparation du téléchargement...",
|
||||||
"downloadedPreview": "Image d'aperçu téléchargée",
|
"downloadedPreview": "Image d'aperçu téléchargée",
|
||||||
"downloadingFile": "Téléchargement du fichier {type}",
|
"downloadingFile": "Téléchargement du fichier {type}",
|
||||||
"finalizing": "Finalisation du téléchargement..."
|
"finalizing": "Finalisation du téléchargement...",
|
||||||
|
"cancelling": "Annulation du téléchargement...",
|
||||||
|
"cancelled": "Téléchargement annulé"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "Fichier actuel :",
|
"currentFile": "Fichier actuel :",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "Images d'exemple {action} terminées",
|
"imagesCompleted": "Images d'exemple {action} terminées",
|
||||||
"imagesFailed": "Images d'exemple {action} échouées",
|
"imagesFailed": "Images d'exemple {action} échouées",
|
||||||
"loadError": "Erreur lors du chargement des téléchargements : {message}",
|
"loadError": "Erreur lors du chargement des téléchargements : {message}",
|
||||||
"downloadError": "Erreur de téléchargement : {message}"
|
"downloadError": "Erreur de téléchargement : {message}",
|
||||||
|
"downloadStopped": "Téléchargement annulé"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "Échec du chargement de l'arborescence des dossiers",
|
"folderTreeFailed": "Échec du chargement de l'arborescence des dossiers",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "נתיבי תיקיות נוספים עודכנו. נדרשת הפעלה מחדש כדי להחיל את השינויים.",
|
"saveSuccess": "נתיבי תיקיות נוספים עודכנו. נדרשת הפעלה מחדש כדי להחיל את השינויים.",
|
||||||
"saveError": "נכשל בעדכון נתיבי תיקיות נוספים: {message}",
|
"saveError": "נכשל בעדכון נתיבי תיקיות נוספים: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "נתיב זה כבר מוגדר"
|
"duplicatePath": "נתיב זה כבר מוגדר",
|
||||||
|
"checkpointUnetOverlap": "לא ניתן להשתמש באותו נתיב עבור checkpoints ומודלי דיפוזיה: {paths}",
|
||||||
|
"checkpointUnetOverlapInline": "הנתיב הזה כבר נמצא בשימוש עבור סוג מודל אחר. יש להשתמש בתיקיות נפרדות עבור checkpoints ומודלי דיפוזיה."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"priorityTags": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "מכין הורדה...",
|
"preparing": "מכין הורדה...",
|
||||||
"downloadedPreview": "תמונת תצוגה מקדימה הורדה",
|
"downloadedPreview": "תמונת תצוגה מקדימה הורדה",
|
||||||
"downloadingFile": "מוריד קובץ {type}",
|
"downloadingFile": "מוריד קובץ {type}",
|
||||||
"finalizing": "מסיים הורדה..."
|
"finalizing": "מסיים הורדה...",
|
||||||
|
"cancelling": "מבטל הורדה...",
|
||||||
|
"cancelled": "ההורדה בוטלה"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "הקובץ הנוכחי:",
|
"currentFile": "הקובץ הנוכחי:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "{action} תמונות הדוגמה הושלם",
|
"imagesCompleted": "{action} תמונות הדוגמה הושלם",
|
||||||
"imagesFailed": "{action} תמונות הדוגמה נכשל",
|
"imagesFailed": "{action} תמונות הדוגמה נכשל",
|
||||||
"loadError": "שגיאה בטעינת הורדות: {message}",
|
"loadError": "שגיאה בטעינת הורדות: {message}",
|
||||||
"downloadError": "שגיאת הורדה: {message}"
|
"downloadError": "שגיאת הורדה: {message}",
|
||||||
|
"downloadStopped": "ההורדה בוטלה"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "טעינת עץ התיקיות נכשלה",
|
"folderTreeFailed": "טעינת עץ התיקיות נכשלה",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "追加フォルダーパスを更新しました。変更を適用するには再起動が必要です。",
|
"saveSuccess": "追加フォルダーパスを更新しました。変更を適用するには再起動が必要です。",
|
||||||
"saveError": "追加フォルダーパスの更新に失敗しました: {message}",
|
"saveError": "追加フォルダーパスの更新に失敗しました: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "このパスはすでに設定されています"
|
"duplicatePath": "このパスはすでに設定されています",
|
||||||
|
"checkpointUnetOverlap": "checkpoints と diffusion models に同じパスは使用できません:{paths}",
|
||||||
|
"checkpointUnetOverlapInline": "このパスは別のモデルタイプですでに使用されています。checkpoints と diffusion models には別々のフォルダを使用してください。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"priorityTags": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "ダウンロードを準備中...",
|
"preparing": "ダウンロードを準備中...",
|
||||||
"downloadedPreview": "プレビュー画像をダウンロードしました",
|
"downloadedPreview": "プレビュー画像をダウンロードしました",
|
||||||
"downloadingFile": "{type}ファイルをダウンロード中",
|
"downloadingFile": "{type}ファイルをダウンロード中",
|
||||||
"finalizing": "ダウンロードを完了中..."
|
"finalizing": "ダウンロードを完了中...",
|
||||||
|
"cancelling": "ダウンロードをキャンセル中...",
|
||||||
|
"cancelled": "ダウンロードをキャンセルしました"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "現在のファイル:",
|
"currentFile": "現在のファイル:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "例画像 {action} が完了しました",
|
"imagesCompleted": "例画像 {action} が完了しました",
|
||||||
"imagesFailed": "例画像 {action} が失敗しました",
|
"imagesFailed": "例画像 {action} が失敗しました",
|
||||||
"loadError": "ダウンロード読み込みエラー:{message}",
|
"loadError": "ダウンロード読み込みエラー:{message}",
|
||||||
"downloadError": "ダウンロードエラー:{message}"
|
"downloadError": "ダウンロードエラー:{message}",
|
||||||
|
"downloadStopped": "ダウンロードをキャンセルしました"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "フォルダツリーの読み込みに失敗しました",
|
"folderTreeFailed": "フォルダツリーの読み込みに失敗しました",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "추가 폴다 경로가 업데이트되었습니다. 변경 사항을 적용하려면 재시작이 필요합니다.",
|
"saveSuccess": "추가 폴다 경로가 업데이트되었습니다. 변경 사항을 적용하려면 재시작이 필요합니다.",
|
||||||
"saveError": "추가 폴다 경로 업데이트 실패: {message}",
|
"saveError": "추가 폴다 경로 업데이트 실패: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "이 경로는 이미 구성되어 있습니다"
|
"duplicatePath": "이 경로는 이미 구성되어 있습니다",
|
||||||
|
"checkpointUnetOverlap": "checkpoints와 diffusion models에 동일한 경로를 사용할 수 없습니다: {paths}",
|
||||||
|
"checkpointUnetOverlapInline": "이 경로는 다른 모델 유형에 이미 사용 중입니다. checkpoints와 diffusion models에 별도의 폴더를 사용하세요."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"priorityTags": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "다운로드 준비 중...",
|
"preparing": "다운로드 준비 중...",
|
||||||
"downloadedPreview": "미리보기 이미지 다운로드됨",
|
"downloadedPreview": "미리보기 이미지 다운로드됨",
|
||||||
"downloadingFile": "{type} 파일 다운로드 중",
|
"downloadingFile": "{type} 파일 다운로드 중",
|
||||||
"finalizing": "다운로드 완료 중..."
|
"finalizing": "다운로드 완료 중...",
|
||||||
|
"cancelling": "다운로드 취소 중...",
|
||||||
|
"cancelled": "다운로드가 취소되었습니다"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "현재 파일:",
|
"currentFile": "현재 파일:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "예시 이미지 {action}이(가) 완료되었습니다",
|
"imagesCompleted": "예시 이미지 {action}이(가) 완료되었습니다",
|
||||||
"imagesFailed": "예시 이미지 {action}이(가) 실패했습니다",
|
"imagesFailed": "예시 이미지 {action}이(가) 실패했습니다",
|
||||||
"loadError": "다운로드 로딩 오류: {message}",
|
"loadError": "다운로드 로딩 오류: {message}",
|
||||||
"downloadError": "다운로드 오류: {message}"
|
"downloadError": "다운로드 오류: {message}",
|
||||||
|
"downloadStopped": "다운로드가 취소되었습니다"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "폴더 트리 로딩 실패",
|
"folderTreeFailed": "폴더 트리 로딩 실패",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "Дополнительные пути к папкам обновлены. Требуется перезапуск для применения изменений.",
|
"saveSuccess": "Дополнительные пути к папкам обновлены. Требуется перезапуск для применения изменений.",
|
||||||
"saveError": "Не удалось обновить дополнительные пути к папкам: {message}",
|
"saveError": "Не удалось обновить дополнительные пути к папкам: {message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "Этот путь уже настроен"
|
"duplicatePath": "Этот путь уже настроен",
|
||||||
|
"checkpointUnetOverlap": "Нельзя использовать один и тот же путь для checkpoints и diffusion models: {paths}",
|
||||||
|
"checkpointUnetOverlapInline": "Этот путь уже используется для другого типа модели. Используйте отдельные папки для checkpoints и diffusion models."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"priorityTags": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "Подготовка загрузки...",
|
"preparing": "Подготовка загрузки...",
|
||||||
"downloadedPreview": "Превью изображение загружено",
|
"downloadedPreview": "Превью изображение загружено",
|
||||||
"downloadingFile": "Загрузка файла {type}",
|
"downloadingFile": "Загрузка файла {type}",
|
||||||
"finalizing": "Завершение загрузки..."
|
"finalizing": "Завершение загрузки...",
|
||||||
|
"cancelling": "Отмена загрузки...",
|
||||||
|
"cancelled": "Загрузка отменена"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "Текущий файл:",
|
"currentFile": "Текущий файл:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "Примеры изображений {action} завершены",
|
"imagesCompleted": "Примеры изображений {action} завершены",
|
||||||
"imagesFailed": "Примеры изображений {action} не удались",
|
"imagesFailed": "Примеры изображений {action} не удались",
|
||||||
"loadError": "Ошибка загрузки downloads: {message}",
|
"loadError": "Ошибка загрузки downloads: {message}",
|
||||||
"downloadError": "Ошибка загрузки: {message}"
|
"downloadError": "Ошибка загрузки: {message}",
|
||||||
|
"downloadStopped": "Загрузка отменена"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "Не удалось загрузить дерево папок",
|
"folderTreeFailed": "Не удалось загрузить дерево папок",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "额外文件夹路径已更新,需要重启才能生效。",
|
"saveSuccess": "额外文件夹路径已更新,需要重启才能生效。",
|
||||||
"saveError": "更新额外文件夹路径失败:{message}",
|
"saveError": "更新额外文件夹路径失败:{message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "此路径已配置"
|
"duplicatePath": "此路径已配置",
|
||||||
|
"checkpointUnetOverlap": "checkpoints 和 diffusion models 不能使用相同的路径:{paths}",
|
||||||
|
"checkpointUnetOverlapInline": "此路径已被用于另一种模型类型。请为 checkpoints 和 diffusion models 使用不同的文件夹。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"priorityTags": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "正在准备下载...",
|
"preparing": "正在准备下载...",
|
||||||
"downloadedPreview": "预览图片已下载",
|
"downloadedPreview": "预览图片已下载",
|
||||||
"downloadingFile": "正在下载 {type} 文件",
|
"downloadingFile": "正在下载 {type} 文件",
|
||||||
"finalizing": "正在完成下载..."
|
"finalizing": "正在完成下载...",
|
||||||
|
"cancelling": "取消下载中...",
|
||||||
|
"cancelled": "下载已取消"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "当前文件:",
|
"currentFile": "当前文件:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "示例图片{action}完成",
|
"imagesCompleted": "示例图片{action}完成",
|
||||||
"imagesFailed": "示例图片{action}失败",
|
"imagesFailed": "示例图片{action}失败",
|
||||||
"loadError": "加载下载项出错:{message}",
|
"loadError": "加载下载项出错:{message}",
|
||||||
"downloadError": "下载错误:{message}"
|
"downloadError": "下载错误:{message}",
|
||||||
|
"downloadStopped": "下载已取消"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "加载文件夹树失败",
|
"folderTreeFailed": "加载文件夹树失败",
|
||||||
|
|||||||
@@ -505,7 +505,9 @@
|
|||||||
"saveSuccess": "額外資料夾路徑已更新,需要重啟才能生效。",
|
"saveSuccess": "額外資料夾路徑已更新,需要重啟才能生效。",
|
||||||
"saveError": "更新額外資料夾路徑失敗:{message}",
|
"saveError": "更新額外資料夾路徑失敗:{message}",
|
||||||
"validation": {
|
"validation": {
|
||||||
"duplicatePath": "此路徑已設定"
|
"duplicatePath": "此路徑已設定",
|
||||||
|
"checkpointUnetOverlap": "checkpoints 和 diffusion models 不能使用相同的路徑:{paths}",
|
||||||
|
"checkpointUnetOverlapInline": "此路徑已被用於另一種模型類型。請為 checkpoints 和 diffusion models 使用不同的資料夾。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"priorityTags": {
|
"priorityTags": {
|
||||||
@@ -1205,7 +1207,9 @@
|
|||||||
"preparing": "準備下載中...",
|
"preparing": "準備下載中...",
|
||||||
"downloadedPreview": "已下載預覽圖片",
|
"downloadedPreview": "已下載預覽圖片",
|
||||||
"downloadingFile": "正在下載 {type} 檔案",
|
"downloadingFile": "正在下載 {type} 檔案",
|
||||||
"finalizing": "完成下載中..."
|
"finalizing": "完成下載中...",
|
||||||
|
"cancelling": "取消下載中...",
|
||||||
|
"cancelled": "下載已取消"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"currentFile": "目前檔案:",
|
"currentFile": "目前檔案:",
|
||||||
@@ -2013,7 +2017,8 @@
|
|||||||
"imagesCompleted": "範例圖片{action}完成",
|
"imagesCompleted": "範例圖片{action}完成",
|
||||||
"imagesFailed": "範例圖片{action}失敗",
|
"imagesFailed": "範例圖片{action}失敗",
|
||||||
"loadError": "載入下載時發生錯誤:{message}",
|
"loadError": "載入下載時發生錯誤:{message}",
|
||||||
"downloadError": "下載錯誤:{message}"
|
"downloadError": "下載錯誤:{message}",
|
||||||
|
"downloadStopped": "下載已取消"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"folderTreeFailed": "載入資料夾樹狀結構失敗",
|
"folderTreeFailed": "載入資料夾樹狀結構失敗",
|
||||||
|
|||||||
@@ -573,12 +573,18 @@ class NodeRegistry:
|
|||||||
tab_nodes[nd["unique_id"]] = nd
|
tab_nodes[nd["unique_id"]] = nd
|
||||||
|
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
|
prev_count = len(self._tab_nodes.get(sid, {}))
|
||||||
self._tab_nodes[sid] = tab_nodes
|
self._tab_nodes[sid] = tab_nodes
|
||||||
self._waiting_clients.discard(sid)
|
self._waiting_clients.discard(sid)
|
||||||
if not self._waiting_clients:
|
if not self._waiting_clients:
|
||||||
self._ready.set()
|
self._ready.set()
|
||||||
|
total_tabs = len(self._tab_nodes)
|
||||||
|
|
||||||
logger.debug("Registered %s nodes from client %s", len(nodes), sid)
|
if len(nodes) != prev_count or len(nodes) > 0:
|
||||||
|
logger.debug(
|
||||||
|
"[LM:Registry] stored %s nodes (was %s) for client %s (total tabs: %s)",
|
||||||
|
len(nodes), prev_count, sid, total_tabs,
|
||||||
|
)
|
||||||
|
|
||||||
def prepare_for_refresh(self, active_sids: list[str]) -> None:
|
def prepare_for_refresh(self, active_sids: list[str]) -> None:
|
||||||
"""Set the list of client IDs we expect to hear from during the next refresh cycle."""
|
"""Set the list of client IDs we expect to hear from during the next refresh cycle."""
|
||||||
@@ -601,10 +607,17 @@ class NodeRegistry:
|
|||||||
longer connected."""
|
longer connected."""
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
# Garbage-collect stale entries (disconnected tabs)
|
# Garbage-collect stale entries (disconnected tabs)
|
||||||
|
stale_sids = []
|
||||||
if active_sids is not None:
|
if active_sids is not None:
|
||||||
for sid in list(self._tab_nodes):
|
for sid in list(self._tab_nodes):
|
||||||
if sid not in active_sids:
|
if sid not in active_sids:
|
||||||
|
stale_sids.append(sid)
|
||||||
del self._tab_nodes[sid]
|
del self._tab_nodes[sid]
|
||||||
|
if stale_sids:
|
||||||
|
logger.debug(
|
||||||
|
"[LM:Registry] GC pruned %s disconnected tabs: %s",
|
||||||
|
len(stale_sids), stale_sids,
|
||||||
|
)
|
||||||
|
|
||||||
merged: dict[str, dict] = {}
|
merged: dict[str, dict] = {}
|
||||||
tab_info: dict[str, dict] = {}
|
tab_info: dict[str, dict] = {}
|
||||||
@@ -3116,6 +3129,8 @@ class NodeRegistryHandler:
|
|||||||
self._node_registry = node_registry
|
self._node_registry = node_registry
|
||||||
self._prompt_server = prompt_server
|
self._prompt_server = prompt_server
|
||||||
self._standalone_mode = standalone_mode
|
self._standalone_mode = standalone_mode
|
||||||
|
self._refresh_lock = asyncio.Lock()
|
||||||
|
self._last_slow_path_ts: float = 0.0
|
||||||
|
|
||||||
async def register_nodes(self, request: web.Request) -> web.Response:
|
async def register_nodes(self, request: web.Request) -> web.Response:
|
||||||
try:
|
try:
|
||||||
@@ -3162,7 +3177,12 @@ class NodeRegistryHandler:
|
|||||||
)
|
)
|
||||||
graph_name = node.get("graph_name")
|
graph_name = node.get("graph_name")
|
||||||
try:
|
try:
|
||||||
node["node_id"] = int(node_id)
|
# Handle compound node IDs from expanded group subgraphs,
|
||||||
|
# e.g. "252:0" → 0 (parent scope is already in graph_id)
|
||||||
|
if isinstance(node_id, str) and ":" in node_id:
|
||||||
|
node["node_id"] = int(node_id.rsplit(":", 1)[-1])
|
||||||
|
else:
|
||||||
|
node["node_id"] = int(node_id)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
@@ -3203,42 +3223,101 @@ class NodeRegistryHandler:
|
|||||||
status=503,
|
status=503,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Snapshot of currently-connected ComfyUI tabs
|
|
||||||
active_sids = list(self._prompt_server.instance.sockets.keys())
|
|
||||||
self._node_registry.prepare_for_refresh(active_sids)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._prompt_server.instance.send_sync("lora_registry_refresh", {})
|
|
||||||
logger.debug(
|
|
||||||
"Sent registry refresh request (expecting %s clients)", len(active_sids)
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error("Failed to send registry refresh message: %s", exc)
|
|
||||||
return web.json_response(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"error": "Communication Error",
|
|
||||||
"message": f"Failed to communicate with ComfyUI frontend: {exc}",
|
|
||||||
},
|
|
||||||
status=500,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not await self._node_registry.wait_for_all(timeout=2.0):
|
|
||||||
logger.warning(
|
|
||||||
"Registry refresh timeout after 2s (%s/%s clients responded)",
|
|
||||||
len(active_sids) - self._node_registry.pending_client_count,
|
|
||||||
len(active_sids),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Re-read current sockets after the wait: a tab may have connected
|
|
||||||
# while we were waiting, and we don't want to garbage-collect it.
|
|
||||||
current_sids = set(self._prompt_server.instance.sockets.keys())
|
current_sids = set(self._prompt_server.instance.sockets.keys())
|
||||||
|
|
||||||
|
# Fast path: if the frontend has already pushed node data (via
|
||||||
|
# afterConfigureGraph / graphChanged hooks), return it immediately
|
||||||
|
# without triggering a WebSocket round-trip.
|
||||||
registry_info = await self._node_registry.get_merged_registry(
|
registry_info = await self._node_registry.get_merged_registry(
|
||||||
active_sids=current_sids
|
active_sids=current_sids
|
||||||
)
|
)
|
||||||
|
if registry_info["tab_count"] > 0:
|
||||||
|
logger.debug(
|
||||||
|
"[LM:Registry] fast path: %s nodes across %s tabs %s",
|
||||||
|
registry_info["node_count"],
|
||||||
|
registry_info["tab_count"],
|
||||||
|
dict(registry_info.get("tabs", {})),
|
||||||
|
)
|
||||||
|
return web.json_response({"success": True, "data": registry_info})
|
||||||
|
|
||||||
|
# Slow path: registry is empty — trigger refresh via WebSocket.
|
||||||
|
# Serialize with an async lock so concurrent callers don't all
|
||||||
|
# trigger separate WS refresh cycles. The second caller will
|
||||||
|
# re-check the fast path and (usually) find populated data.
|
||||||
|
async with self._refresh_lock:
|
||||||
|
# Re-check after acquiring the lock — another concurrent call
|
||||||
|
# may have populated the cache while we were waiting.
|
||||||
|
registry_info = await self._node_registry.get_merged_registry(
|
||||||
|
active_sids=current_sids
|
||||||
|
)
|
||||||
|
if registry_info["tab_count"] > 0:
|
||||||
|
logger.debug(
|
||||||
|
"[LM:Registry] fast path after lock wait: %s nodes across %s tabs",
|
||||||
|
registry_info["node_count"],
|
||||||
|
registry_info["tab_count"],
|
||||||
|
)
|
||||||
|
return web.json_response({"success": True, "data": registry_info})
|
||||||
|
|
||||||
|
# Cooldown: if the slow path ran recently (< 2 s) and
|
||||||
|
# returned empty, skip another WS round-trip.
|
||||||
|
elapsed = time.monotonic() - self._last_slow_path_ts
|
||||||
|
if elapsed < 2.0:
|
||||||
|
logger.debug(
|
||||||
|
"[LM:Registry] slow path cooldown (%.1fs since last refresh), returning empty",
|
||||||
|
elapsed,
|
||||||
|
)
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": "Empty Registry",
|
||||||
|
"message": "No workflow nodes found — ensure ComfyUI is open and the extension is loaded.",
|
||||||
|
},
|
||||||
|
status=408,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"[LM:Registry] slow path: cache empty, triggering WS refresh (%s connected tabs: %s)",
|
||||||
|
len(current_sids), list(current_sids)[:5],
|
||||||
|
)
|
||||||
|
active_sids = list(current_sids)
|
||||||
|
self._node_registry.prepare_for_refresh(active_sids)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._prompt_server.instance.send_sync("lora_registry_refresh", {})
|
||||||
|
logger.debug(
|
||||||
|
"Sent registry refresh request (expecting %s clients)", len(active_sids)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Failed to send registry refresh message: %s", exc)
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": "Communication Error",
|
||||||
|
"message": f"Failed to communicate with ComfyUI frontend: {exc}",
|
||||||
|
},
|
||||||
|
status=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not await self._node_registry.wait_for_all(timeout=0.5):
|
||||||
|
logger.warning(
|
||||||
|
"Registry refresh timeout after 0.5s (%s/%s clients responded)",
|
||||||
|
len(active_sids) - self._node_registry.pending_client_count,
|
||||||
|
len(active_sids),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Re-read current sockets after the wait: a tab may have connected
|
||||||
|
# while we were waiting, and we don't want to garbage-collect it.
|
||||||
|
current_sids = set(self._prompt_server.instance.sockets.keys())
|
||||||
|
registry_info = await self._node_registry.get_merged_registry(
|
||||||
|
active_sids=current_sids
|
||||||
|
)
|
||||||
|
self._last_slow_path_ts = time.monotonic()
|
||||||
|
|
||||||
if registry_info["node_count"] == 0:
|
if registry_info["node_count"] == 0:
|
||||||
logger.warning("No nodes registered after refresh")
|
logger.debug(
|
||||||
|
"[LM:Registry] refresh OK — %s connected tab(s) but 0 compatible nodes found",
|
||||||
|
registry_info["tab_count"],
|
||||||
|
)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
|
|||||||
@@ -1313,9 +1313,20 @@ class ModelQueryHandler:
|
|||||||
}
|
}
|
||||||
if include_license_flags:
|
if include_license_flags:
|
||||||
model_data = await self._service.get_model_info_by_name(model_name)
|
model_data = await self._service.get_model_info_by_name(model_name)
|
||||||
license_flags = (model_data or {}).get("license_flags")
|
# Only return license_flags when real CivitAI model license
|
||||||
if license_flags is not None:
|
# data exists. This mirrors ModelModal's guard
|
||||||
response_payload["license_flags"] = int(license_flags)
|
# (modelData?.civitai?.model) so the preview tooltip never
|
||||||
|
# shows misleading license icons for HF or other models
|
||||||
|
# without actual license metadata.
|
||||||
|
civitai_data = (model_data or {}).get("civitai") or {}
|
||||||
|
has_license_data = (
|
||||||
|
isinstance(civitai_data, dict)
|
||||||
|
and isinstance(civitai_data.get("model"), dict)
|
||||||
|
)
|
||||||
|
if has_license_data:
|
||||||
|
license_flags = (model_data or {}).get("license_flags")
|
||||||
|
if license_flags is not None:
|
||||||
|
response_payload["license_flags"] = int(license_flags)
|
||||||
# Include the user's license icon style preference so the
|
# Include the user's license icon style preference so the
|
||||||
# ComfyUI tooltip can pick the right set without a separate
|
# ComfyUI tooltip can pick the right set without a separate
|
||||||
# API call.
|
# API call.
|
||||||
|
|||||||
@@ -230,6 +230,12 @@ class DownloadManager:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict with download result
|
Dict with download result
|
||||||
"""
|
"""
|
||||||
|
logger.debug(
|
||||||
|
"[download] download_from_civitai called: model_id=%s, model_version_id=%s, "
|
||||||
|
"source=%s, file_params=%s",
|
||||||
|
model_id, model_version_id, source, file_params,
|
||||||
|
)
|
||||||
|
|
||||||
# Validate that at least one identifier is provided
|
# Validate that at least one identifier is provided
|
||||||
if not model_id and not model_version_id:
|
if not model_id and not model_version_id:
|
||||||
return {
|
return {
|
||||||
@@ -250,6 +256,7 @@ class DownloadManager:
|
|||||||
"source": source,
|
"source": source,
|
||||||
"file_params": copy.deepcopy(file_params) if file_params is not None else None,
|
"file_params": copy.deepcopy(file_params) if file_params is not None else None,
|
||||||
"progress": 0,
|
"progress": 0,
|
||||||
|
|
||||||
"status": "queued",
|
"status": "queued",
|
||||||
"transfer_backend": self._get_model_download_backend(),
|
"transfer_backend": self._get_model_download_backend(),
|
||||||
"bytes_downloaded": 0,
|
"bytes_downloaded": 0,
|
||||||
@@ -289,8 +296,8 @@ class DownloadManager:
|
|||||||
return result
|
return result
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": True,
|
||||||
"error": "Download was cancelled",
|
"cancelled": True,
|
||||||
"download_id": task_id,
|
"download_id": task_id,
|
||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
@@ -1421,14 +1428,35 @@ class DownloadManager:
|
|||||||
|
|
||||||
# If file_params is provided, try to find matching file
|
# If file_params is provided, try to find matching file
|
||||||
if file_params and model_version_id:
|
if file_params and model_version_id:
|
||||||
|
target_file_id = file_params.get("id")
|
||||||
target_type = file_params.get("type", "Model")
|
target_type = file_params.get("type", "Model")
|
||||||
target_format = file_params.get("format", "SafeTensor")
|
target_format = file_params.get("format")
|
||||||
target_size = file_params.get("size", "full")
|
target_size = file_params.get("size")
|
||||||
target_fp = file_params.get("fp")
|
target_fp = file_params.get("fp")
|
||||||
is_primary = file_params.get("isPrimary", False)
|
is_primary = file_params.get("isPrimary", False)
|
||||||
|
|
||||||
if is_primary:
|
logger.debug(
|
||||||
# Find primary file
|
"[download] file_params received: id=%s, type=%s, format=%s, size=%s, fp=%s, isPrimary=%s, "
|
||||||
|
"model_version_id=%s, total_files=%d",
|
||||||
|
target_file_id, target_type, target_format, target_size, target_fp, is_primary,
|
||||||
|
model_version_id, len(files),
|
||||||
|
)
|
||||||
|
|
||||||
|
if target_file_id:
|
||||||
|
target_id_str = str(target_file_id)
|
||||||
|
for f in files:
|
||||||
|
f_id = f.get("id")
|
||||||
|
if str(f_id) == target_id_str:
|
||||||
|
file_info = f
|
||||||
|
logger.debug(
|
||||||
|
"[download] MATCH by ID: id=%s name='%s'",
|
||||||
|
f_id, f.get("name"),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
if not file_info:
|
||||||
|
logger.debug("[download] No file found with id=%s", target_file_id)
|
||||||
|
|
||||||
|
elif is_primary:
|
||||||
file_info = next(
|
file_info = next(
|
||||||
(
|
(
|
||||||
f
|
f
|
||||||
@@ -1439,28 +1467,41 @@ class DownloadManager:
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Match by metadata
|
# Lenient metadata match: only compare fields present on both sides
|
||||||
for f in files:
|
for f in files:
|
||||||
f_type = f.get("type", "")
|
f_type = f.get("type", "")
|
||||||
f_meta = f.get("metadata", {})
|
|
||||||
|
|
||||||
# Check type match
|
|
||||||
if f_type != target_type:
|
if f_type != target_type:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check metadata match
|
f_meta = f.get("metadata", {})
|
||||||
if f_meta.get("format") != target_format:
|
f_format = f_meta.get("format") or f.get("format")
|
||||||
|
f_size = f_meta.get("size") or f.get("size")
|
||||||
|
f_fp = f_meta.get("fp") or f.get("fp")
|
||||||
|
|
||||||
|
if target_format and f_format != target_format:
|
||||||
continue
|
continue
|
||||||
if f_meta.get("size") != target_size:
|
if target_size and f_size and f_size != target_size:
|
||||||
continue
|
continue
|
||||||
if target_fp and f_meta.get("fp") != target_fp:
|
if target_fp and f_fp and f_fp != target_fp:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_info = f
|
file_info = f
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if not file_info:
|
||||||
|
logger.debug(
|
||||||
|
"[download] No match found via file_params — falling back to primary file lookup",
|
||||||
|
)
|
||||||
|
elif not file_params:
|
||||||
|
logger.debug(
|
||||||
|
"[download] No file_params provided (null/None) — will use primary file lookup. "
|
||||||
|
"model_version_id=%s, total_files=%d",
|
||||||
|
model_version_id, len(files),
|
||||||
|
)
|
||||||
|
|
||||||
# Fallback to primary file if no match found
|
# Fallback to primary file if no match found
|
||||||
if not file_info:
|
if not file_info:
|
||||||
|
logger.debug("[download] Looking for primary file as fallback")
|
||||||
file_info = next(
|
file_info = next(
|
||||||
(
|
(
|
||||||
f
|
f
|
||||||
@@ -1469,6 +1510,13 @@ class DownloadManager:
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
if file_info:
|
||||||
|
logger.debug(
|
||||||
|
"[download] Fallback primary file selected: id=%s, name=%s",
|
||||||
|
file_info.get("id"), file_info.get("name"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug("[download] No primary file found in fallback lookup")
|
||||||
|
|
||||||
if not file_info:
|
if not file_info:
|
||||||
return {"success": False, "error": "No suitable file found in metadata"}
|
return {"success": False, "error": "No suitable file found in metadata"}
|
||||||
|
|||||||
@@ -630,12 +630,37 @@ class SettingsManager:
|
|||||||
|
|
||||||
return False
|
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(
|
def _validate_folder_paths(
|
||||||
self,
|
self,
|
||||||
library_name: str,
|
library_name: str,
|
||||||
folder_paths: Mapping[str, Iterable[str]],
|
folder_paths: Mapping[str, Iterable[str]],
|
||||||
) -> None:
|
) -> 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", {})
|
libraries = self.settings.get("libraries", {})
|
||||||
normalized_new: Dict[str, Dict[str, str]] = {}
|
normalized_new: Dict[str, Dict[str, str]] = {}
|
||||||
for key, values in folder_paths.items():
|
for key, values in folder_paths.items():
|
||||||
@@ -673,6 +698,22 @@ class SettingsManager:
|
|||||||
f"Folder path(s) {collisions} already assigned to library '{other_name}'"
|
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(
|
def _update_active_library_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -1547,8 +1588,12 @@ class SettingsManager:
|
|||||||
portable_switch_pending = True
|
portable_switch_pending = True
|
||||||
self._prepare_portable_switch(value)
|
self._prepare_portable_switch(value)
|
||||||
if key == "folder_paths" and isinstance(value, Mapping):
|
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]
|
self._update_active_library_entry(folder_paths=value) # type: ignore[arg-type]
|
||||||
elif key == "extra_folder_paths" and isinstance(value, Mapping):
|
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]
|
self._update_active_library_entry(extra_folder_paths=value) # type: ignore[arg-type]
|
||||||
elif key == "default_lora_root":
|
elif key == "default_lora_root":
|
||||||
self._update_active_library_entry(default_lora_root=str(value))
|
self._update_active_library_entry(default_lora_root=str(value))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-lora-manager"
|
name = "comfyui-lora-manager"
|
||||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -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);
|
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 {
|
.extra-folder-path-row .path-controls .remove-path-btn {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|||||||
@@ -112,6 +112,18 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelDownload(downloadId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${DOWNLOAD_ENDPOINTS.cancelGet}?download_id=${encodeURIComponent(downloadId)}`
|
||||||
|
);
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cancelling download:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadMoreWithVirtualScroll(resetPage = false, updateFolders = false) {
|
async loadMoreWithVirtualScroll(resetPage = false, updateFolders = false) {
|
||||||
const pageState = this.getPageState();
|
const pageState = this.getPageState();
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,17 @@ export class BulkMissingLoraDownloadManager {
|
|||||||
let completedDownloads = 0;
|
let completedDownloads = 0;
|
||||||
let failedDownloads = 0;
|
let failedDownloads = 0;
|
||||||
let currentLoraProgress = 0;
|
let currentLoraProgress = 0;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
loadingManager.showCancelButton(async () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
cancelled = true;
|
||||||
|
try {
|
||||||
|
await this.loraApiClient.cancelDownload(batchDownloadId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Cancel request failed:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Set up WebSocket message handler
|
// Set up WebSocket message handler
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@@ -207,6 +218,11 @@ export class BulkMissingLoraDownloadManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.status === 'cancelled') {
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Process progress updates
|
// Process progress updates
|
||||||
if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) {
|
if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) {
|
||||||
currentLoraProgress = data.progress;
|
currentLoraProgress = data.progress;
|
||||||
@@ -249,6 +265,8 @@ export class BulkMissingLoraDownloadManager {
|
|||||||
|
|
||||||
// Download each LoRA sequentially
|
// Download each LoRA sequentially
|
||||||
for (let i = 0; i < lorasToDownload.length; i++) {
|
for (let i = 0; i < lorasToDownload.length; i++) {
|
||||||
|
if (cancelled) break;
|
||||||
|
|
||||||
const lora = lorasToDownload[i];
|
const lora = lorasToDownload[i];
|
||||||
|
|
||||||
currentLoraProgress = 0;
|
currentLoraProgress = 0;
|
||||||
@@ -275,11 +293,13 @@ export class BulkMissingLoraDownloadManager {
|
|||||||
modelId,
|
modelId,
|
||||||
versionId,
|
versionId,
|
||||||
loraRoot,
|
loraRoot,
|
||||||
'', // Empty relative path, use default paths
|
'',
|
||||||
useDefaultPaths,
|
useDefaultPaths,
|
||||||
batchDownloadId
|
batchDownloadId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (cancelled) break;
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
console.error(`Failed to download LoRA ${lora.name || lora.file_name}: ${response.error}`);
|
console.error(`Failed to download LoRA ${lora.name || lora.file_name}: ${response.error}`);
|
||||||
failedDownloads++;
|
failedDownloads++;
|
||||||
@@ -288,8 +308,10 @@ export class BulkMissingLoraDownloadManager {
|
|||||||
updateProgress(100, completedDownloads, '');
|
updateProgress(100, completedDownloads, '');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error downloading LoRA ${lora.name || lora.file_name}:`, error);
|
if (!cancelled) {
|
||||||
failedDownloads++;
|
console.error(`Error downloading LoRA ${lora.name || lora.file_name}:`, error);
|
||||||
|
failedDownloads++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +322,10 @@ export class BulkMissingLoraDownloadManager {
|
|||||||
loadingManager.hide();
|
loadingManager.hide();
|
||||||
|
|
||||||
// Show completion message
|
// Show completion message
|
||||||
if (failedDownloads === 0) {
|
if (cancelled) {
|
||||||
|
showToast('toast.downloads.downloadStopped', {}, 'info',
|
||||||
|
`Download cancelled. ${completedDownloads} item(s) completed.`);
|
||||||
|
} else if (failedDownloads === 0) {
|
||||||
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
||||||
} else {
|
} else {
|
||||||
showToast('toast.loras.downloadPartialSuccess', {
|
showToast('toast.loras.downloadPartialSuccess', {
|
||||||
|
|||||||
@@ -728,14 +728,23 @@ export class DownloadManager {
|
|||||||
|
|
||||||
confirmFileSelection() {
|
confirmFileSelection() {
|
||||||
const selectedRadio = document.querySelector('#fileSelectionList input[type="radio"]:checked');
|
const selectedRadio = document.querySelector('#fileSelectionList input[type="radio"]:checked');
|
||||||
if (!selectedRadio) return;
|
if (!selectedRadio) {
|
||||||
|
console.warn('[download] confirmFileSelection: no radio button checked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const version = this.currentVersion;
|
const version = this.currentVersion;
|
||||||
if (!version) return;
|
if (!version) {
|
||||||
|
console.warn('[download] confirmFileSelection: no currentVersion set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const modelFiles = (version.files || []).filter(f => f.type === 'Model' || f.type === 'UNet' || f.type === 'Diffusion Model');
|
const modelFiles = (version.files || []).filter(f => f.type === 'Model' || f.type === 'UNet' || f.type === 'Diffusion Model');
|
||||||
this.selectedFile = modelFiles.find(f => f.id.toString() === selectedRadio.value);
|
this.selectedFile = modelFiles.find(f => f.id.toString() === selectedRadio.value);
|
||||||
|
|
||||||
|
console.log('[download] confirmFileSelection: selected file id=%s, name="%s", type="%s", metadata=%o',
|
||||||
|
this.selectedFile?.id, this.selectedFile?.name, this.selectedFile?.type, this.selectedFile?.metadata);
|
||||||
|
|
||||||
document.getElementById('fileSelectionStep').style.display = 'none';
|
document.getElementById('fileSelectionStep').style.display = 'none';
|
||||||
document.getElementById('locationStep').style.display = 'block';
|
document.getElementById('locationStep').style.display = 'block';
|
||||||
this.proceedToLocationContent();
|
this.proceedToLocationContent();
|
||||||
@@ -872,16 +881,26 @@ export class DownloadManager {
|
|||||||
const displayName = versionName || `#${versionId}`;
|
const displayName = versionName || `#${versionId}`;
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let updateProgress = () => { };
|
let updateProgress = () => { };
|
||||||
|
let cancelled = false;
|
||||||
|
const downloadId = Date.now().toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.loadingManager.restoreProgressBar();
|
this.loadingManager.restoreProgressBar();
|
||||||
updateProgress = this.loadingManager.showDownloadProgress(1);
|
updateProgress = this.loadingManager.showDownloadProgress(1);
|
||||||
updateProgress(0, 0, displayName);
|
updateProgress(0, 0, displayName);
|
||||||
|
|
||||||
const downloadId = Date.now().toString();
|
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
|
ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
|
||||||
|
|
||||||
|
this.loadingManager.showCancelButton(async () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
cancelled = true;
|
||||||
|
try {
|
||||||
|
await this.apiClient.cancelDownload(downloadId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Cancel request failed:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ws.onmessage = event => {
|
ws.onmessage = event => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
@@ -890,6 +909,12 @@ export class DownloadManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.status === 'cancelled') {
|
||||||
|
cancelled = true;
|
||||||
|
this.loadingManager.setStatus(translate('modals.download.status.cancelled', {}, 'Download cancelled'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.status === 'progress' && data.download_id === downloadId) {
|
if (data.status === 'progress' && data.download_id === downloadId) {
|
||||||
const metrics = {
|
const metrics = {
|
||||||
bytesDownloaded: data.bytes_downloaded,
|
bytesDownloaded: data.bytes_downloaded,
|
||||||
@@ -928,6 +953,10 @@ export class DownloadManager {
|
|||||||
fileParams
|
fileParams
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (response?.skipped) {
|
if (response?.skipped) {
|
||||||
this.loadingManager.setStatus(translate('modals.download.status.finalizing'));
|
this.loadingManager.setStatus(translate('modals.download.status.finalizing'));
|
||||||
updateProgress(100, 0, displayName);
|
updateProgress(100, 0, displayName);
|
||||||
@@ -968,8 +997,12 @@ export class DownloadManager {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to download model version:', error);
|
if (cancelled) {
|
||||||
showToast('toast.downloads.downloadError', { message: error?.message }, 'error');
|
console.log('Download cancelled by user:', downloadId);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to download model version:', error);
|
||||||
|
showToast('toast.downloads.downloadError', { message: error?.message }, 'error');
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
@@ -989,16 +1022,33 @@ export class DownloadManager {
|
|||||||
const totalFiles = this.hfSelectedFiles.length;
|
const totalFiles = this.hfSelectedFiles.length;
|
||||||
const updateProgress = this.loadingManager.showDownloadProgress(totalFiles);
|
const updateProgress = this.loadingManager.showDownloadProgress(totalFiles);
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
let currentDownloadId = null;
|
||||||
|
|
||||||
|
this.loadingManager.showCancelButton(async () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
cancelled = true;
|
||||||
|
if (currentDownloadId) {
|
||||||
|
try {
|
||||||
|
await this.apiClient.cancelDownload(currentDownloadId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Cancel request failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let completedDownloads = 0;
|
let completedDownloads = 0;
|
||||||
for (let i = 0; i < totalFiles; i++) {
|
for (let i = 0; i < totalFiles; i++) {
|
||||||
|
if (cancelled) break;
|
||||||
|
|
||||||
const filename = this.hfSelectedFiles[i];
|
const filename = this.hfSelectedFiles[i];
|
||||||
updateProgress(0, completedDownloads, filename);
|
updateProgress(0, completedDownloads, filename);
|
||||||
this.loadingManager.setStatus(`Downloading ${filename}...`);
|
this.loadingManager.setStatus(`Downloading ${filename}...`);
|
||||||
|
|
||||||
const downloadId = Date.now().toString() + '_' + i;
|
currentDownloadId = Date.now().toString() + '_' + i;
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
|
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${currentDownloadId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
@@ -1006,12 +1056,13 @@ export class DownloadManager {
|
|||||||
ws.onerror = reject;
|
ws.onerror = reject;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Capture completed count at WS creation time so progress
|
|
||||||
// updates arriving after completedDownloads increments still
|
|
||||||
// show the correct "N / total" position.
|
|
||||||
const snapshotCompleted = completedDownloads;
|
const snapshotCompleted = completedDownloads;
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.status === 'cancelled') {
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.status === 'progress') {
|
if (data.status === 'progress') {
|
||||||
const metrics = {
|
const metrics = {
|
||||||
bytesDownloaded: data.bytes_downloaded,
|
bytesDownloaded: data.bytes_downloaded,
|
||||||
@@ -1029,9 +1080,11 @@ export class DownloadManager {
|
|||||||
modelRoot,
|
modelRoot,
|
||||||
relativePath: targetFolder,
|
relativePath: targetFolder,
|
||||||
useDefaultPaths,
|
useDefaultPaths,
|
||||||
download_id: downloadId,
|
download_id: currentDownloadId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (cancelled) break;
|
||||||
|
|
||||||
if (response?.success) {
|
if (response?.success) {
|
||||||
completedDownloads++;
|
completedDownloads++;
|
||||||
updateProgress(100, completedDownloads, filename);
|
updateProgress(100, completedDownloads, filename);
|
||||||
@@ -1041,13 +1094,19 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('toast.loras.downloadCompleted', {}, 'success');
|
if (cancelled) {
|
||||||
// Reload page data — model is already in scanner cache via backend
|
showToast('toast.downloads.downloadStopped', {}, 'info',
|
||||||
|
`Download cancelled. ${completedDownloads} item(s) completed.`);
|
||||||
|
} else {
|
||||||
|
showToast('toast.loras.downloadCompleted', {}, 'success');
|
||||||
|
}
|
||||||
await resetAndReload(true);
|
await resetAndReload(true);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to download HF model:', error);
|
if (!cancelled) {
|
||||||
showToast('toast.downloads.downloadError', { message: error?.message }, 'error');
|
console.error('Failed to download HF model:', error);
|
||||||
|
showToast('toast.downloads.downloadError', { message: error?.message }, 'error');
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingManager.hide();
|
this.loadingManager.hide();
|
||||||
@@ -1426,12 +1485,23 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileParams = this.selectedFile ? {
|
const fileParams = this.selectedFile ? {
|
||||||
|
id: this.selectedFile.id,
|
||||||
type: this.selectedFile.type || 'Model',
|
type: this.selectedFile.type || 'Model',
|
||||||
format: this.selectedFile.metadata?.format || 'SafeTensor',
|
format: this.selectedFile.metadata?.format || null,
|
||||||
size: this.selectedFile.metadata?.size || 'full',
|
size: this.selectedFile.metadata?.size || null,
|
||||||
fp: this.selectedFile.metadata?.fp,
|
fp: this.selectedFile.metadata?.fp || null,
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
|
if (fileParams) {
|
||||||
|
console.log('[download] startDownload (single): fileParams built from selectedFile — id=%s, type=%s, format=%s, size=%s, fp=%s',
|
||||||
|
fileParams.id, fileParams.type, fileParams.format, fileParams.size, fileParams.fp);
|
||||||
|
} else {
|
||||||
|
console.log('[download] startDownload (single): this.selectedFile is null — no file selection, will download primary/default file. version=%s has %d files',
|
||||||
|
this.currentVersion?.id, (this.currentVersion?.files || []).length);
|
||||||
|
}
|
||||||
|
|
||||||
|
modalManager.closeModal('downloadModal');
|
||||||
|
|
||||||
return this.executeDownloadWithProgress({
|
return this.executeDownloadWithProgress({
|
||||||
modelId: this.modelId,
|
modelId: this.modelId,
|
||||||
versionId: this.currentVersion.id,
|
versionId: this.currentVersion.id,
|
||||||
@@ -1470,11 +1540,27 @@ export class DownloadManager {
|
|||||||
|
|
||||||
let completedDownloads = 0;
|
let completedDownloads = 0;
|
||||||
let failedDownloads = 0;
|
let failedDownloads = 0;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
loadingManager.showCancelButton(async () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
cancelled = true;
|
||||||
|
try {
|
||||||
|
await this.apiClient.cancelDownload(batchDownloadId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Cancel request failed:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
if (data.type === 'download_id') return;
|
if (data.type === 'download_id') return;
|
||||||
|
|
||||||
|
if (data.status === 'cancelled') {
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.status === 'progress' && data.download_id?.startsWith(batchDownloadId)) {
|
if (data.status === 'progress' && data.download_id?.startsWith(batchDownloadId)) {
|
||||||
const current = downloadItems[completedDownloads + failedDownloads];
|
const current = downloadItems[completedDownloads + failedDownloads];
|
||||||
const name = current?.selectedVersion?.name || current?.displayName || current?.filename || `#${completedDownloads + failedDownloads + 1}`;
|
const name = current?.selectedVersion?.name || current?.displayName || current?.filename || `#${completedDownloads + failedDownloads + 1}`;
|
||||||
@@ -1493,6 +1579,8 @@ export class DownloadManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < downloadItems.length; i++) {
|
for (let i = 0; i < downloadItems.length; i++) {
|
||||||
|
if (cancelled) break;
|
||||||
|
|
||||||
const item = downloadItems[i];
|
const item = downloadItems[i];
|
||||||
const name = item.displayName || item.filename || (item.selectedVersion?.name || `Model #${item.modelId}`);
|
const name = item.displayName || item.filename || (item.selectedVersion?.name || `Model #${item.modelId}`);
|
||||||
const isHf = item.source === 'huggingface';
|
const isHf = item.source === 'huggingface';
|
||||||
@@ -1503,7 +1591,6 @@ export class DownloadManager {
|
|||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
if (isHf) {
|
if (isHf) {
|
||||||
// Per-file WebSocket for real-time progress
|
|
||||||
const downloadId = Date.now().toString() + '_hf_' + i;
|
const downloadId = Date.now().toString() + '_hf_' + i;
|
||||||
const wsHf = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
|
const wsHf = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
|
||||||
try {
|
try {
|
||||||
@@ -1537,6 +1624,8 @@ export class DownloadManager {
|
|||||||
wsHf.close();
|
wsHf.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.log('[download] batch download: fileParams NOT passed for modelId=%s, versionId=%s — backend will use primary file',
|
||||||
|
item.modelId, item.selectedVersion?.id);
|
||||||
response = await this.apiClient.downloadModel(
|
response = await this.apiClient.downloadModel(
|
||||||
item.modelId,
|
item.modelId,
|
||||||
item.selectedVersion.id,
|
item.selectedVersion.id,
|
||||||
@@ -1548,6 +1637,8 @@ export class DownloadManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cancelled) break;
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
failedDownloads++;
|
failedDownloads++;
|
||||||
} else {
|
} else {
|
||||||
@@ -1555,15 +1646,20 @@ export class DownloadManager {
|
|||||||
updateProgress(100, completedDownloads, '');
|
updateProgress(100, completedDownloads, '');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to download ${name}:`, err);
|
if (!cancelled) {
|
||||||
failedDownloads++;
|
console.error(`Failed to download ${name}:`, err);
|
||||||
|
failedDownloads++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.close();
|
ws.close();
|
||||||
loadingManager.hide();
|
loadingManager.hide();
|
||||||
|
|
||||||
if (failedDownloads === 0) {
|
if (cancelled) {
|
||||||
|
showToast('toast.downloads.downloadStopped', {}, 'info',
|
||||||
|
`Download cancelled. ${completedDownloads} item(s) completed.`);
|
||||||
|
} else if (failedDownloads === 0) {
|
||||||
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
||||||
} else {
|
} else {
|
||||||
showToast('toast.loras.downloadPartialSuccess', {
|
showToast('toast.loras.downloadPartialSuccess', {
|
||||||
@@ -1581,6 +1677,10 @@ export class DownloadManager {
|
|||||||
modelRoot = '',
|
modelRoot = '',
|
||||||
targetFolder = ''
|
targetFolder = ''
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
console.warn('[download] downloadVersionWithDefaults: NO fileParams will be sent — backend will always use primary file. '
|
||||||
|
+ 'modelType=%s, modelId=%s, versionId=%s, versionName="%s"',
|
||||||
|
modelType, modelId, versionId, versionName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.apiClient = getModelApiClient(modelType);
|
this.apiClient = getModelApiClient(modelType);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -281,6 +281,10 @@ export class LoadingManager {
|
|||||||
// Initialize transfer stats with empty data
|
// Initialize transfer stats with empty data
|
||||||
updateTransferStats();
|
updateTransferStats();
|
||||||
|
|
||||||
|
if (this.cancelButton) {
|
||||||
|
this.loadingContent.appendChild(this.cancelButton);
|
||||||
|
}
|
||||||
|
|
||||||
// Return update function
|
// Return update function
|
||||||
return (currentProgress, currentIndex = 0, currentName = '', metrics = {}) => {
|
return (currentProgress, currentIndex = 0, currentName = '', metrics = {}) => {
|
||||||
// Update current item progress
|
// Update current item progress
|
||||||
|
|||||||
@@ -1693,13 +1693,15 @@ export class SettingsManager {
|
|||||||
<input type="text" class="extra-folder-path-input"
|
<input type="text" class="extra-folder-path-input"
|
||||||
placeholder="${translate('settings.extraFolderPaths.pathPlaceholder', {}, '/path/to/models')}" value="${path}"
|
placeholder="${translate('settings.extraFolderPaths.pathPlaceholder', {}, '/path/to/models')}" value="${path}"
|
||||||
onblur="settingsManager.updateExtraFolderPaths('${modelType}')"
|
onblur="settingsManager.updateExtraFolderPaths('${modelType}')"
|
||||||
|
onfocus="settingsManager.clearExtraFolderPathError(this)"
|
||||||
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
|
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
|
||||||
<button type="button" class="remove-path-btn"
|
<button type="button" class="remove-path-btn"
|
||||||
onclick="this.parentElement.parentElement.remove(); settingsManager.updateExtraFolderPaths('${modelType}')"
|
onclick="settingsManager.removeExtraFolderPathRow(this, '${modelType}')"
|
||||||
title="${translate('common.actions.delete', {}, 'Delete')}">
|
title="${translate('common.actions.delete', {}, 'Delete')}">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="extra-folder-path-error"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.appendChild(row);
|
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) {
|
async updateExtraFolderPaths(changedModelType) {
|
||||||
|
// Clear previous errors
|
||||||
|
this._clearAllExtraFolderPathErrors();
|
||||||
|
|
||||||
const extraFolderPaths = {};
|
const extraFolderPaths = {};
|
||||||
|
|
||||||
// Collect paths for all model types
|
// Collect paths for all model types
|
||||||
@@ -1734,6 +1792,32 @@ export class SettingsManager {
|
|||||||
extraFolderPaths[modelType] = paths;
|
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
|
// Check if paths have actually changed
|
||||||
const currentPaths = state.global.settings.extra_folder_paths || {};
|
const currentPaths = state.global.settings.extra_folder_paths || {};
|
||||||
const pathsChanged = JSON.stringify(currentPaths) !== JSON.stringify(extraFolderPaths);
|
const pathsChanged = JSON.stringify(currentPaths) !== JSON.stringify(extraFolderPaths);
|
||||||
|
|||||||
@@ -168,6 +168,18 @@ export class DownloadManager {
|
|||||||
let failedDownloads = 0;
|
let failedDownloads = 0;
|
||||||
let accessFailures = 0;
|
let accessFailures = 0;
|
||||||
let currentLoraProgress = 0;
|
let currentLoraProgress = 0;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
this.importManager.loadingManager.showCancelButton(async () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
cancelled = true;
|
||||||
|
try {
|
||||||
|
const loraClient = getModelApiClient(MODEL_TYPES.LORA);
|
||||||
|
await loraClient.cancelDownload(batchDownloadId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Cancel request failed:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Set up progress tracking for current download
|
// Set up progress tracking for current download
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@@ -179,6 +191,11 @@ export class DownloadManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.status === 'cancelled') {
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Process progress updates for our current active download
|
// Process progress updates for our current active download
|
||||||
if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) {
|
if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) {
|
||||||
// Update current LoRA progress
|
// Update current LoRA progress
|
||||||
@@ -221,6 +238,8 @@ export class DownloadManager {
|
|||||||
const useDefaultPaths = getStorageItem('use_default_path_loras', false);
|
const useDefaultPaths = getStorageItem('use_default_path_loras', false);
|
||||||
|
|
||||||
for (let i = 0; i < this.importManager.downloadableLoRAs.length; i++) {
|
for (let i = 0; i < this.importManager.downloadableLoRAs.length; i++) {
|
||||||
|
if (cancelled) break;
|
||||||
|
|
||||||
const lora = this.importManager.downloadableLoRAs[i];
|
const lora = this.importManager.downloadableLoRAs[i];
|
||||||
|
|
||||||
// Reset current LoRA progress for new download
|
// Reset current LoRA progress for new download
|
||||||
@@ -241,15 +260,13 @@ export class DownloadManager {
|
|||||||
batchDownloadId
|
batchDownloadId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (cancelled) break;
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
console.error(`Failed to download LoRA ${lora.name}: ${response.error}`);
|
console.error(`Failed to download LoRA ${lora.name}: ${response.error}`);
|
||||||
|
|
||||||
failedDownloads++;
|
failedDownloads++;
|
||||||
// Continue with next download
|
|
||||||
} else {
|
} else {
|
||||||
completedDownloads++;
|
completedDownloads++;
|
||||||
|
|
||||||
// Update progress to show completion of current LoRA
|
|
||||||
updateProgress(100, completedDownloads, '');
|
updateProgress(100, completedDownloads, '');
|
||||||
|
|
||||||
if (completedDownloads + failedDownloads < this.importManager.downloadableLoRAs.length) {
|
if (completedDownloads + failedDownloads < this.importManager.downloadableLoRAs.length) {
|
||||||
@@ -259,9 +276,10 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (downloadError) {
|
} catch (downloadError) {
|
||||||
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
|
if (!cancelled) {
|
||||||
failedDownloads++;
|
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
|
||||||
// Continue with next download
|
failedDownloads++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +287,10 @@ export class DownloadManager {
|
|||||||
ws.close();
|
ws.close();
|
||||||
|
|
||||||
// Show appropriate completion message based on results
|
// Show appropriate completion message based on results
|
||||||
if (failedDownloads === 0) {
|
if (cancelled) {
|
||||||
|
showToast('toast.downloads.downloadStopped', {}, 'info',
|
||||||
|
`Download cancelled. ${completedDownloads} item(s) completed.`);
|
||||||
|
} else if (failedDownloads === 0) {
|
||||||
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
||||||
} else {
|
} else {
|
||||||
if (accessFailures > 0) {
|
if (accessFailures > 0) {
|
||||||
|
|||||||
@@ -552,6 +552,8 @@ async function fetchWorkflowRegistry() {
|
|||||||
if (!registryData.success) {
|
if (!registryData.success) {
|
||||||
if (registryData.error === 'Standalone Mode Active') {
|
if (registryData.error === 'Standalone Mode Active') {
|
||||||
showToast('toast.general.cannotInteractStandalone', {}, 'warning');
|
showToast('toast.general.cannotInteractStandalone', {}, 'warning');
|
||||||
|
} else if (registryData.error === 'Empty Registry') {
|
||||||
|
showToast('uiHelpers.workflow.noSupportedNodes', {}, 'warning');
|
||||||
} else {
|
} else {
|
||||||
showToast('toast.general.failedWorkflowInfo', {}, 'error');
|
showToast('toast.general.failedWorkflowInfo', {}, 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,10 @@
|
|||||||
<a href="https://github.com/willmiao/ComfyUI-Lora-Manager/wiki/Priority-Tags-Configuration-Guide" target="_blank">
|
<a href="https://github.com/willmiao/ComfyUI-Lora-Manager/wiki/Priority-Tags-Configuration-Guide" target="_blank">
|
||||||
Priority Tags Configuration Guide
|
Priority Tags Configuration Guide
|
||||||
<span class="new-content-badge inline">{{ t('help.documentation.newBadge') }}</span>
|
<span class="new-content-badge inline">{{ t('help.documentation.newBadge') }}</span>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/willmiao/ComfyUI-Lora-Manager/wiki/AI-Provider-Setup" target="_blank">
|
||||||
|
AI Provider Setup
|
||||||
|
<span class="new-content-badge inline">{{ t('help.documentation.newBadge') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -728,6 +728,54 @@ async def test_register_nodes_includes_capabilities():
|
|||||||
assert stored_node["widget_names"] == ["ckpt_name"]
|
assert stored_node["widget_names"] == ["ckpt_name"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_register_nodes_accepts_compound_node_ids():
|
||||||
|
"""Subgraph nodes from expanded group nodes have compound IDs like '252:0'."""
|
||||||
|
node_registry = NodeRegistry()
|
||||||
|
handler = NodeRegistryHandler(
|
||||||
|
node_registry=node_registry,
|
||||||
|
prompt_server=FakePromptServer,
|
||||||
|
standalone_mode=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
request = FakeRequest(
|
||||||
|
json_data={
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"node_id": "252:0",
|
||||||
|
"graph_id": "252",
|
||||||
|
"type": "CheckpointLoaderSimple",
|
||||||
|
"title": "Checkpoint Loader (subgraph)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_id": "252:1",
|
||||||
|
"graph_id": "252",
|
||||||
|
"type": "CLIPLoader",
|
||||||
|
"title": "CLIP Loader (subgraph)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"client_id": "test-client-1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await handler.register_nodes(request)
|
||||||
|
payload = json.loads(response.text)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert "2 nodes registered" in payload["message"]
|
||||||
|
|
||||||
|
registry = await node_registry.get_merged_registry()
|
||||||
|
assert registry["node_count"] == 2
|
||||||
|
|
||||||
|
nodes_map = registry["nodes"]
|
||||||
|
assert "252:0" in nodes_map
|
||||||
|
assert "252:1" in nodes_map
|
||||||
|
assert nodes_map["252:0"]["id"] == 0
|
||||||
|
assert nodes_map["252:0"]["graph_id"] == "252"
|
||||||
|
assert nodes_map["252:1"]["id"] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_node_widget_sends_payload():
|
async def test_update_node_widget_sends_payload():
|
||||||
send_calls: list[tuple[str, dict]] = []
|
send_calls: list[tuple[str, dict]] = []
|
||||||
|
|||||||
@@ -133,6 +133,14 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Vue node mode: prevent content from pushing node size via ResizeObserver.
|
||||||
|
Same technique as .lm-loras-container.lm-vue-node above. */
|
||||||
|
.comfy-tags-container.lm-vue-node {
|
||||||
|
height: 100%;
|
||||||
|
min-height: var(--comfy-widget-min-height, 150px);
|
||||||
|
contain: layout size;
|
||||||
|
}
|
||||||
|
|
||||||
.lm-lora-empty-state {
|
.lm-lora-empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { forwardMiddleMouseToCanvas, forwardWheelToCanvas } from "./utils.js";
|
import { forwardMiddleMouseToCanvas, forwardWheelToCanvas } from "./utils.js";
|
||||||
import { copyToClipboard } from "./loras_widget_utils.js";
|
import { copyToClipboard } from "./loras_widget_utils.js";
|
||||||
|
import { ensureLmStyles } from "./lm_styles_loader.js";
|
||||||
|
|
||||||
const MIN_HEIGHT = 150;
|
const MIN_HEIGHT = 150;
|
||||||
const GROUP_EDITOR_ID = "lm-trigger-group-editor";
|
const GROUP_EDITOR_ID = "lm-trigger-group-editor";
|
||||||
@@ -696,6 +697,16 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set a fixed minimum height so the node has a reasonable starting size.
|
||||||
|
// Adding or removing tags does NOT change the node size — the container
|
||||||
|
// scrolls when content exceeds the allocated space.
|
||||||
|
ensureLmStyles();
|
||||||
|
container.style.setProperty("--comfy-widget-min-height", `${MIN_HEIGHT}px`);
|
||||||
|
|
||||||
|
if (typeof LiteGraph !== "undefined" && LiteGraph.vueNodesMode) {
|
||||||
|
container.classList.add("lm-vue-node");
|
||||||
|
}
|
||||||
|
|
||||||
const initialTagsData = opts?.defaultVal || [];
|
const initialTagsData = opts?.defaultVal || [];
|
||||||
|
|
||||||
function renderSimpleTag(tagData, index, widget, showStrengthInfo) {
|
function renderSimpleTag(tagData, index, widget, showStrengthInfo) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { getAllGraphNodes, getNodeReference, getNodeFromGraph } from "./utils.js";
|
import { getAllGraphNodes, getNodeReference, getNodeFromGraph, chainCallback } from "./utils.js";
|
||||||
import { ensureLmStyles } from "./lm_styles_loader.js";
|
import { ensureLmStyles } from "./lm_styles_loader.js";
|
||||||
|
|
||||||
|
const DEBOUNCE_DELAY = 500;
|
||||||
|
|
||||||
const LORA_NODE_CLASSES = new Set([
|
const LORA_NODE_CLASSES = new Set([
|
||||||
"Lora Loader (LoraManager)",
|
"Lora Loader (LoraManager)",
|
||||||
"Lora Stacker (LoraManager)",
|
"Lora Stacker (LoraManager)",
|
||||||
@@ -79,22 +81,77 @@ app.registerExtension({
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
ensureLmStyles();
|
ensureLmStyles();
|
||||||
|
this._log("extension initialized, clientId=%s", api.clientId ?? api.initialClientId ?? "(pending)");
|
||||||
|
|
||||||
api.addEventListener("lora_registry_refresh", () => {
|
api.addEventListener("lora_registry_refresh", () => {
|
||||||
this.refreshRegistry();
|
this.refreshRegistry(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
api.addEventListener("lm_widget_update", (event) => {
|
api.addEventListener("lm_widget_update", (event) => {
|
||||||
this.applyWidgetUpdate(event?.detail ?? {});
|
this.applyWidgetUpdate(event?.detail ?? {});
|
||||||
});
|
});
|
||||||
|
|
||||||
// React to marker changes from the Node Marker extension
|
|
||||||
window.addEventListener("lm_marker_changed", () => {
|
window.addEventListener("lm_marker_changed", () => {
|
||||||
this.refreshRegistry();
|
this.refreshRegistry();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._hookGraphChanges();
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshRegistry() {
|
async afterConfigureGraph(_missingNodeTypes, _app) {
|
||||||
|
this._log("afterConfigureGraph: workflow loaded (%s missing types)", _missingNodeTypes?.length ?? 0);
|
||||||
|
await this.refreshRegistry();
|
||||||
|
},
|
||||||
|
|
||||||
|
_hookGraphChanges() {
|
||||||
|
const graph = app.graph;
|
||||||
|
if (!graph) {
|
||||||
|
this._log("app.graph not available, skipping proactive hooks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hooksInstalled = 0;
|
||||||
|
|
||||||
|
const scheduleRefresh = (source) => {
|
||||||
|
if (this._debounceTimer != null) {
|
||||||
|
clearTimeout(this._debounceTimer);
|
||||||
|
}
|
||||||
|
this._debounceTimer = setTimeout(() => {
|
||||||
|
this._debounceTimer = null;
|
||||||
|
this.refreshRegistry();
|
||||||
|
}, DEBOUNCE_DELAY);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
chainCallback(graph, "onNodeAdded", () => scheduleRefresh("onNodeAdded"));
|
||||||
|
chainCallback(graph, "onNodeRemoved", () => scheduleRefresh("onNodeRemoved"));
|
||||||
|
hooksInstalled += 2;
|
||||||
|
} catch (e) {
|
||||||
|
this._log("failed to chain LiteGraph hooks: %s", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof api.addEventListener === "function") {
|
||||||
|
try {
|
||||||
|
api.addEventListener("graphChanged", () => scheduleRefresh("graphChanged"));
|
||||||
|
hooksInstalled += 1;
|
||||||
|
} catch (_e) {
|
||||||
|
// graphChanged may not be available on older ComfyUI versions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._log("%s proactive hooks installed on graph", hooksInstalled);
|
||||||
|
},
|
||||||
|
|
||||||
|
_log(format, ...args) {
|
||||||
|
const ts = new Date().toISOString().slice(11, 23);
|
||||||
|
let msg = format;
|
||||||
|
for (const arg of args) {
|
||||||
|
msg = msg.replace(/%s/g, String(arg));
|
||||||
|
}
|
||||||
|
console.debug(`[LM:Registry ${ts}] ${msg}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshRegistry(force = false) {
|
||||||
try {
|
try {
|
||||||
const workflowNodes = [];
|
const workflowNodes = [];
|
||||||
const nodeEntries = getAllGraphNodes(app.graph);
|
const nodeEntries = getAllGraphNodes(app.graph);
|
||||||
@@ -115,7 +172,6 @@ app.registerExtension({
|
|||||||
const hasTextWidget = TEXT_CAPABLE_CLASSES.has(node.comfyClass);
|
const hasTextWidget = TEXT_CAPABLE_CLASSES.has(node.comfyClass);
|
||||||
const markerRole = node.properties?.lm_marker_role ?? null;
|
const markerRole = node.properties?.lm_marker_role ?? null;
|
||||||
|
|
||||||
// Skip nodes with no relevant capability UNLESS they are marked
|
|
||||||
if (!supportsLora && !hasTargetWidget && !hasTextWidget && !markerRole) {
|
if (!supportsLora && !hasTargetWidget && !hasTextWidget && !markerRole) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -146,6 +202,19 @@ app.registerExtension({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientId = api.clientId ?? api.initialClientId ?? "";
|
||||||
|
|
||||||
|
// Content-based dedup: skip POST if identical to last sent payload,
|
||||||
|
// unless forced (e.g. responding to a lora_registry_refresh WS message
|
||||||
|
// where the backend explicitly requests a re-registration).
|
||||||
|
const fingerprint = JSON.stringify(
|
||||||
|
workflowNodes.map(n => `${n.graph_id}:${n.node_id}|${n.marker_role ?? ""}|${n.mode ?? 0}`).sort()
|
||||||
|
);
|
||||||
|
if (!force && fingerprint === this._lastFingerprint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._lastFingerprint = fingerprint;
|
||||||
|
|
||||||
const response = await fetch("/api/lm/register-nodes", {
|
const response = await fetch("/api/lm/register-nodes", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -153,7 +222,7 @@ app.registerExtension({
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
nodes: workflowNodes,
|
nodes: workflowNodes,
|
||||||
client_id: api.clientId ?? api.initialClientId ?? "",
|
client_id: clientId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user