mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(auto-organize): add auto-organize functionality for selected models and update context menu
This commit is contained in:
@@ -324,8 +324,18 @@
|
||||
"copyAll": "Alle Syntax kopieren",
|
||||
"refreshAll": "Alle Metadaten aktualisieren",
|
||||
"moveAll": "Alle in Ordner verschieben",
|
||||
"autoOrganize": "Automatisch organisieren",
|
||||
"deleteAll": "Alle Modelle löschen",
|
||||
"clear": "Auswahl löschen"
|
||||
"clear": "Auswahl löschen",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "Automatische Organisation wird initialisiert...",
|
||||
"starting": "Automatische Organisation für {type} wird gestartet...",
|
||||
"processing": "Verarbeitung ({processed}/{total}) – {success} verschoben, {skipped} übersprungen, {failures} fehlgeschlagen",
|
||||
"cleaning": "Leere Verzeichnisse werden bereinigt...",
|
||||
"completed": "Abgeschlossen: {success} verschoben, {skipped} übersprungen, {failures} fehlgeschlagen",
|
||||
"complete": "Automatische Organisation abgeschlossen",
|
||||
"error": "Fehler: {error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitai-Daten aktualisieren",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "{completed} von {total} LoRAs heruntergeladen. {accessFailures} fehlgeschlagen aufgrund von Zugriffsbeschränkungen. Überprüfen Sie Ihren API-Schlüssel in den Einstellungen oder den Early Access-Status.",
|
||||
"pleaseSelectVersion": "Bitte wählen Sie eine Version aus",
|
||||
"versionExists": "Diese Version existiert bereits in Ihrer Bibliothek",
|
||||
"downloadCompleted": "Download erfolgreich abgeschlossen"
|
||||
"downloadCompleted": "Download erfolgreich abgeschlossen",
|
||||
"autoOrganizeSuccess": "Automatische Organisation für {count} {type} erfolgreich abgeschlossen",
|
||||
"autoOrganizePartialSuccess": "Automatische Organisation abgeschlossen: {success} verschoben, {failures} fehlgeschlagen von insgesamt {total} Modellen",
|
||||
"autoOrganizeFailed": "Automatische Organisation fehlgeschlagen: {error}",
|
||||
"noModelsSelected": "Keine Modelle ausgewählt"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "Fehler beim Abrufen der Rezepte: {message}",
|
||||
|
||||
@@ -324,8 +324,18 @@
|
||||
"copyAll": "Copy All Syntax",
|
||||
"refreshAll": "Refresh All Metadata",
|
||||
"moveAll": "Move All to Folder",
|
||||
"autoOrganize": "Auto-Organize Selected",
|
||||
"deleteAll": "Delete All Models",
|
||||
"clear": "Clear Selection"
|
||||
"clear": "Clear Selection",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "Initializing auto-organize...",
|
||||
"starting": "Starting auto-organize for {type}...",
|
||||
"processing": "Processing ({processed}/{total}) - {success} moved, {skipped} skipped, {failures} failed",
|
||||
"cleaning": "Cleaning up empty directories...",
|
||||
"completed": "Completed: {success} moved, {skipped} skipped, {failures} failed",
|
||||
"complete": "Auto-organize complete",
|
||||
"error": "Error: {error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Refresh Civitai Data",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "Downloaded {completed} of {total} LoRAs. {accessFailures} failed due to access restrictions. Check your API key in settings or early access status.",
|
||||
"pleaseSelectVersion": "Please select a version",
|
||||
"versionExists": "This version already exists in your library",
|
||||
"downloadCompleted": "Download completed successfully"
|
||||
"downloadCompleted": "Download completed successfully",
|
||||
"autoOrganizeSuccess": "Auto-organize completed successfully for {count} {type}",
|
||||
"autoOrganizePartialSuccess": "Auto-organize completed with {success} moved, {failures} failed out of {total} models",
|
||||
"autoOrganizeFailed": "Auto-organize failed: {error}",
|
||||
"noModelsSelected": "No models selected"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "Failed to fetch recipes: {message}",
|
||||
|
||||
@@ -324,8 +324,18 @@
|
||||
"copyAll": "Copiar toda la sintaxis",
|
||||
"refreshAll": "Actualizar todos los metadatos",
|
||||
"moveAll": "Mover todos a carpeta",
|
||||
"autoOrganize": "Auto-organizar seleccionados",
|
||||
"deleteAll": "Eliminar todos los modelos",
|
||||
"clear": "Limpiar selección"
|
||||
"clear": "Limpiar selección",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "Inicializando auto-organización...",
|
||||
"starting": "Iniciando auto-organización para {type}...",
|
||||
"processing": "Procesando ({processed}/{total}) - {success} movidos, {skipped} omitidos, {failures} fallidos",
|
||||
"cleaning": "Limpiando directorios vacíos...",
|
||||
"completed": "Completado: {success} movidos, {skipped} omitidos, {failures} fallidos",
|
||||
"complete": "Auto-organización completada",
|
||||
"error": "Error: {error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Actualizar datos de Civitai",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "Descargados {completed} de {total} LoRAs. {accessFailures} fallaron debido a restricciones de acceso. Revisa tu clave API en configuración o estado de acceso temprano.",
|
||||
"pleaseSelectVersion": "Por favor selecciona una versión",
|
||||
"versionExists": "Esta versión ya existe en tu biblioteca",
|
||||
"downloadCompleted": "Descarga completada exitosamente"
|
||||
"downloadCompleted": "Descarga completada exitosamente",
|
||||
"autoOrganizeSuccess": "Auto-organización completada exitosamente para {count} {type}",
|
||||
"autoOrganizePartialSuccess": "Auto-organización completada con {success} movidos, {failures} fallidos de un total de {total} modelos",
|
||||
"autoOrganizeFailed": "Auto-organización fallida: {error}",
|
||||
"noModelsSelected": "No hay modelos seleccionados"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "Error al obtener recetas: {message}",
|
||||
|
||||
@@ -324,8 +324,18 @@
|
||||
"copyAll": "Copier toute la syntaxe",
|
||||
"refreshAll": "Actualiser toutes les métadonnées",
|
||||
"moveAll": "Déplacer tout vers un dossier",
|
||||
"autoOrganize": "Auto-organiser la sélection",
|
||||
"deleteAll": "Supprimer tous les modèles",
|
||||
"clear": "Effacer la sélection"
|
||||
"clear": "Effacer la sélection",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "Initialisation de l'auto-organisation...",
|
||||
"starting": "Démarrage de l'auto-organisation pour {type}...",
|
||||
"processing": "Traitement ({processed}/{total}) - {success} déplacés, {skipped} ignorés, {failures} échecs",
|
||||
"cleaning": "Nettoyage des répertoires vides...",
|
||||
"completed": "Terminé : {success} déplacés, {skipped} ignorés, {failures} échecs",
|
||||
"complete": "Auto-organisation terminée",
|
||||
"error": "Erreur : {error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Actualiser les données Civitai",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "{completed} sur {total} LoRAs téléchargés. {accessFailures} ont échoué en raison de restrictions d'accès. Vérifiez votre clé API dans les paramètres ou le statut d'accès anticipé.",
|
||||
"pleaseSelectVersion": "Veuillez sélectionner une version",
|
||||
"versionExists": "Cette version existe déjà dans votre bibliothèque",
|
||||
"downloadCompleted": "Téléchargement terminé avec succès"
|
||||
"downloadCompleted": "Téléchargement terminé avec succès",
|
||||
"autoOrganizeSuccess": "Auto-organisation terminée avec succès pour {count} {type}",
|
||||
"autoOrganizePartialSuccess": "Auto-organisation terminée avec {success} déplacés, {failures} échecs sur {total} modèles",
|
||||
"autoOrganizeFailed": "Échec de l'auto-organisation : {error}",
|
||||
"noModelsSelected": "Aucun modèle sélectionné"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "Échec de la récupération des recipes : {message}",
|
||||
|
||||
@@ -324,8 +324,18 @@
|
||||
"copyAll": "すべての構文をコピー",
|
||||
"refreshAll": "すべてのメタデータを更新",
|
||||
"moveAll": "すべてをフォルダに移動",
|
||||
"autoOrganize": "自動整理を実行",
|
||||
"deleteAll": "すべてのモデルを削除",
|
||||
"clear": "選択をクリア"
|
||||
"clear": "選択をクリア",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "自動整理を初期化中...",
|
||||
"starting": "{type}の自動整理を開始中...",
|
||||
"processing": "処理中({processed}/{total})- {success} 移動、{skipped} スキップ、{failures} 失敗",
|
||||
"cleaning": "空のディレクトリをクリーンアップ中...",
|
||||
"completed": "完了:{success} 移動、{skipped} スキップ、{failures} 失敗",
|
||||
"complete": "自動整理が完了しました",
|
||||
"error": "エラー:{error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitaiデータを更新",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "{total} LoRAのうち {completed} がダウンロードされました。{accessFailures} はアクセス制限により失敗しました。設定でAPIキーまたはアーリーアクセス状況を確認してください。",
|
||||
"pleaseSelectVersion": "バージョンを選択してください",
|
||||
"versionExists": "このバージョンは既にライブラリに存在します",
|
||||
"downloadCompleted": "ダウンロードが正常に完了しました"
|
||||
"downloadCompleted": "ダウンロードが正常に完了しました",
|
||||
"autoOrganizeSuccess": "{count} {type} の自動整理が正常に完了しました",
|
||||
"autoOrganizePartialSuccess": "自動整理が完了しました:{total} モデル中 {success} 移動、{failures} 失敗",
|
||||
"autoOrganizeFailed": "自動整理に失敗しました:{error}",
|
||||
"noModelsSelected": "モデルが選択されていません"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "レシピの取得に失敗しました:{message}",
|
||||
|
||||
@@ -324,8 +324,18 @@
|
||||
"copyAll": "모든 문법 복사",
|
||||
"refreshAll": "모든 메타데이터 새로고침",
|
||||
"moveAll": "모두 폴더로 이동",
|
||||
"autoOrganize": "자동 정리 선택",
|
||||
"deleteAll": "모든 모델 삭제",
|
||||
"clear": "선택 지우기"
|
||||
"clear": "선택 지우기",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "자동 정리 초기화 중...",
|
||||
"starting": "{type}에 대한 자동 정리 시작...",
|
||||
"processing": "처리 중 ({processed}/{total}) - {success}개 이동, {skipped}개 건너뜀, {failures}개 실패",
|
||||
"cleaning": "빈 디렉토리 정리 중...",
|
||||
"completed": "완료: {success}개 이동, {skipped}개 건너뜀, {failures}개 실패",
|
||||
"complete": "자동 정리 완료",
|
||||
"error": "오류: {error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitai 데이터 새로고침",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "{total}개 중 {completed}개 LoRA가 다운로드되었습니다. {accessFailures}개는 액세스 제한으로 실패했습니다. 설정에서 API 키 또는 얼리 액세스 상태를 확인하세요.",
|
||||
"pleaseSelectVersion": "버전을 선택해주세요",
|
||||
"versionExists": "이 버전은 이미 라이브러리에 있습니다",
|
||||
"downloadCompleted": "다운로드가 성공적으로 완료되었습니다"
|
||||
"downloadCompleted": "다운로드가 성공적으로 완료되었습니다",
|
||||
"autoOrganizeSuccess": "{count}개의 {type}에 대해 자동 정리가 성공적으로 완료되었습니다",
|
||||
"autoOrganizePartialSuccess": "자동 정리 완료: 전체 {total}개 중 {success}개 이동, {failures}개 실패",
|
||||
"autoOrganizeFailed": "자동 정리 실패: {error}",
|
||||
"noModelsSelected": "선택된 모델이 없습니다"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "레시피 가져오기 실패: {message}",
|
||||
|
||||
@@ -324,8 +324,18 @@
|
||||
"copyAll": "Копировать весь синтаксис",
|
||||
"refreshAll": "Обновить все метаданные",
|
||||
"moveAll": "Переместить все в папку",
|
||||
"autoOrganize": "Автоматически организовать выбранные",
|
||||
"deleteAll": "Удалить все модели",
|
||||
"clear": "Очистить выбор"
|
||||
"clear": "Очистить выбор",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "Инициализация автоматической организации...",
|
||||
"starting": "Запуск автоматической организации для {type}...",
|
||||
"processing": "Обработка ({processed}/{total}) — {success} перемещено, {skipped} пропущено, {failures} не удалось",
|
||||
"cleaning": "Очистка пустых директорий...",
|
||||
"completed": "Завершено: {success} перемещено, {skipped} пропущено, {failures} не удалось",
|
||||
"complete": "Автоматическая организация завершена",
|
||||
"error": "Ошибка: {error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Обновить данные Civitai",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "Загружено {completed} из {total} LoRAs. {accessFailures} не удалось из-за ограничений доступа. Проверьте ваш API ключ в настройках или статус раннего доступа.",
|
||||
"pleaseSelectVersion": "Пожалуйста, выберите версию",
|
||||
"versionExists": "Эта версия уже существует в вашей библиотеке",
|
||||
"downloadCompleted": "Загрузка успешно завершена"
|
||||
"downloadCompleted": "Загрузка успешно завершена",
|
||||
"autoOrganizeSuccess": "Автоматическая организация успешно завершена для {count} {type}",
|
||||
"autoOrganizePartialSuccess": "Автоматическая организация завершена: перемещено {success}, не удалось {failures} из {total} моделей",
|
||||
"autoOrganizeFailed": "Ошибка автоматической организации: {error}",
|
||||
"noModelsSelected": "Модели не выбраны"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "Не удалось получить рецепты: {message}",
|
||||
|
||||
@@ -324,8 +324,18 @@
|
||||
"copyAll": "复制全部语法",
|
||||
"refreshAll": "刷新全部元数据",
|
||||
"moveAll": "全部移动到文件夹",
|
||||
"autoOrganize": "自动整理所选模型",
|
||||
"deleteAll": "删除所有模型",
|
||||
"clear": "清除选择"
|
||||
"clear": "清除选择",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "正在初始化自动整理...",
|
||||
"starting": "正在为 {type} 启动自动整理...",
|
||||
"processing": "处理中({processed}/{total})- 已移动 {success} 个,跳过 {skipped} 个,失败 {failures} 个",
|
||||
"cleaning": "正在清理空文件夹...",
|
||||
"completed": "完成:已移动 {success} 个,跳过 {skipped} 个,失败 {failures} 个",
|
||||
"complete": "自动整理已完成",
|
||||
"error": "错误:{error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "刷新 Civitai 数据",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "已下载 {completed}/{total} 个 LoRA。{accessFailures} 个因访问限制失败。请检查设置中的 API 密钥或早期访问状态。",
|
||||
"pleaseSelectVersion": "请选择版本",
|
||||
"versionExists": "该版本已存在于你的库中",
|
||||
"downloadCompleted": "下载成功完成"
|
||||
"downloadCompleted": "下载成功完成",
|
||||
"autoOrganizeSuccess": "自动整理已成功完成,共 {count} 个 {type}",
|
||||
"autoOrganizePartialSuccess": "自动整理完成:已移动 {success} 个,{failures} 个失败,共 {total} 个模型",
|
||||
"autoOrganizeFailed": "自动整理失败:{error}",
|
||||
"noModelsSelected": "未选中模型"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "获取配方失败:{message}",
|
||||
|
||||
@@ -324,8 +324,18 @@
|
||||
"copyAll": "複製全部語法",
|
||||
"refreshAll": "刷新全部 metadata",
|
||||
"moveAll": "全部移動到資料夾",
|
||||
"autoOrganize": "自動整理所選模型",
|
||||
"deleteAll": "刪除全部模型",
|
||||
"clear": "清除選取"
|
||||
"clear": "清除選取",
|
||||
"autoOrganizeProgress": {
|
||||
"initializing": "正在初始化自動整理...",
|
||||
"starting": "正在開始自動整理 {type}...",
|
||||
"processing": "處理中({processed}/{total})- 已移動 {success},已略過 {skipped},失敗 {failures}",
|
||||
"cleaning": "正在清理空資料夾...",
|
||||
"completed": "完成:已移動 {success},已略過 {skipped},失敗 {failures}",
|
||||
"complete": "自動整理完成",
|
||||
"error": "錯誤:{error}"
|
||||
}
|
||||
},
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "刷新 Civitai 資料",
|
||||
@@ -942,7 +952,11 @@
|
||||
"downloadPartialWithAccess": "已下載 {completed} 個 LoRA,共 {total} 個。{accessFailures} 個因訪問限制而失敗。請檢查您的 API 密鑰或提前訪問狀態。",
|
||||
"pleaseSelectVersion": "請選擇一個版本",
|
||||
"versionExists": "此版本已存在於您的庫中",
|
||||
"downloadCompleted": "下載成功完成"
|
||||
"downloadCompleted": "下載成功完成",
|
||||
"autoOrganizeSuccess": "自動整理已成功完成,共 {count} 個 {type} 已整理",
|
||||
"autoOrganizePartialSuccess": "自動整理完成:已移動 {success} 個,{failures} 個失敗,共 {total} 個模型",
|
||||
"autoOrganizeFailed": "自動整理失敗:{error}",
|
||||
"noModelsSelected": "未選擇任何模型"
|
||||
},
|
||||
"recipes": {
|
||||
"fetchFailed": "取得配方失敗:{message}",
|
||||
|
||||
@@ -56,6 +56,7 @@ class BaseModelRoutes(ABC):
|
||||
app.router.add_post(f'/api/{prefix}/move_model', self.move_model)
|
||||
app.router.add_post(f'/api/{prefix}/move_models_bulk', self.move_models_bulk)
|
||||
app.router.add_get(f'/api/{prefix}/auto-organize', self.auto_organize_models)
|
||||
app.router.add_post(f'/api/{prefix}/auto-organize', self.auto_organize_models)
|
||||
app.router.add_get(f'/api/{prefix}/auto-organize-progress', self.get_auto_organize_progress)
|
||||
|
||||
# Common query routes
|
||||
@@ -773,7 +774,7 @@ class BaseModelRoutes(ABC):
|
||||
return web.Response(text=str(e), status=500)
|
||||
|
||||
async def auto_organize_models(self, request: web.Request) -> web.Response:
|
||||
"""Auto-organize all models based on current settings"""
|
||||
"""Auto-organize all models or a specific set of models based on current settings"""
|
||||
try:
|
||||
# Check if auto-organize is already running
|
||||
if ws_manager.is_auto_organize_running():
|
||||
@@ -791,8 +792,17 @@ class BaseModelRoutes(ABC):
|
||||
'error': 'Auto-organize is already running. Please wait for it to complete.'
|
||||
}, status=409)
|
||||
|
||||
# Get specific file paths from request if this is a POST with selected models
|
||||
file_paths = None
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
data = await request.json()
|
||||
file_paths = data.get('file_paths')
|
||||
except Exception:
|
||||
pass # Continue with all models if no valid JSON
|
||||
|
||||
async with auto_organize_lock:
|
||||
return await self._perform_auto_organize()
|
||||
return await self._perform_auto_organize(file_paths)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto_organize_models: {e}", exc_info=True)
|
||||
@@ -809,20 +819,33 @@ class BaseModelRoutes(ABC):
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
async def _perform_auto_organize(self) -> web.Response:
|
||||
"""Perform the actual auto-organize operation"""
|
||||
async def _perform_auto_organize(self, file_paths=None) -> web.Response:
|
||||
"""Perform the actual auto-organize operation
|
||||
|
||||
Args:
|
||||
file_paths: Optional list of specific file paths to organize.
|
||||
If None, organizes all models.
|
||||
"""
|
||||
try:
|
||||
# Get all models from cache
|
||||
cache = await self.service.scanner.get_cached_data()
|
||||
all_models = cache.raw_data
|
||||
|
||||
# Filter models if specific file paths are provided
|
||||
if file_paths:
|
||||
all_models = [model for model in all_models if model.get('file_path') in file_paths]
|
||||
operation_type = 'bulk'
|
||||
else:
|
||||
operation_type = 'all'
|
||||
|
||||
# Get model roots for this scanner
|
||||
model_roots = self.service.get_model_roots()
|
||||
if not model_roots:
|
||||
await ws_manager.broadcast_auto_organize_progress({
|
||||
'type': 'auto_organize_progress',
|
||||
'status': 'error',
|
||||
'error': 'No model roots configured'
|
||||
'error': 'No model roots configured',
|
||||
'operation_type': operation_type
|
||||
})
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
@@ -849,7 +872,8 @@ class BaseModelRoutes(ABC):
|
||||
'processed': 0,
|
||||
'success': 0,
|
||||
'failures': 0,
|
||||
'skipped': 0
|
||||
'skipped': 0,
|
||||
'operation_type': operation_type
|
||||
})
|
||||
|
||||
# Process models in batches
|
||||
@@ -980,7 +1004,8 @@ class BaseModelRoutes(ABC):
|
||||
'processed': processed,
|
||||
'success': success_count,
|
||||
'failures': failure_count,
|
||||
'skipped': skipped_count
|
||||
'skipped': skipped_count,
|
||||
'operation_type': operation_type
|
||||
})
|
||||
|
||||
# Small delay between batches to prevent overwhelming the system
|
||||
@@ -995,7 +1020,8 @@ class BaseModelRoutes(ABC):
|
||||
'success': success_count,
|
||||
'failures': failure_count,
|
||||
'skipped': skipped_count,
|
||||
'message': 'Cleaning up empty directories...'
|
||||
'message': 'Cleaning up empty directories...',
|
||||
'operation_type': operation_type
|
||||
})
|
||||
|
||||
# Clean up empty directories after organizing
|
||||
@@ -1014,20 +1040,22 @@ class BaseModelRoutes(ABC):
|
||||
'success': success_count,
|
||||
'failures': failure_count,
|
||||
'skipped': skipped_count,
|
||||
'cleanup': cleanup_counts
|
||||
'cleanup': cleanup_counts,
|
||||
'operation_type': operation_type
|
||||
})
|
||||
|
||||
# Prepare response with limited details
|
||||
response_data = {
|
||||
'success': True,
|
||||
'message': f'Auto-organize completed: {success_count} moved, {skipped_count} skipped, {failure_count} failed out of {total_models} total',
|
||||
'message': f'Auto-organize {operation_type} completed: {success_count} moved, {skipped_count} skipped, {failure_count} failed out of {total_models} total',
|
||||
'summary': {
|
||||
'total': total_models,
|
||||
'success': success_count,
|
||||
'skipped': skipped_count,
|
||||
'failures': failure_count,
|
||||
'organization_type': 'flat' if is_flat_structure else 'structured',
|
||||
'cleaned_dirs': cleanup_counts
|
||||
'cleaned_dirs': cleanup_counts,
|
||||
'operation_type': operation_type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,7 +1075,8 @@ class BaseModelRoutes(ABC):
|
||||
await ws_manager.broadcast_auto_organize_progress({
|
||||
'type': 'auto_organize_progress',
|
||||
'status': 'error',
|
||||
'error': str(e)
|
||||
'error': str(e),
|
||||
'operation_type': operation_type if 'operation_type' in locals() else 'unknown'
|
||||
})
|
||||
|
||||
raise e
|
||||
|
||||
@@ -94,6 +94,10 @@ export function getApiEndpoints(modelType) {
|
||||
metadata: `/api/${modelType}/metadata`,
|
||||
modelDescription: `/api/${modelType}/model-description`,
|
||||
|
||||
// Auto-organize operations
|
||||
autoOrganize: `/api/${modelType}/auto-organize`,
|
||||
autoOrganizeProgress: `/api/${modelType}/auto-organize-progress`,
|
||||
|
||||
// Model-specific endpoints (will be merged with specific configs)
|
||||
specific: {}
|
||||
};
|
||||
|
||||
@@ -1011,23 +1011,140 @@ export class BaseModelApiClient {
|
||||
|
||||
async fetchModelDescription(filePath) {
|
||||
try {
|
||||
const params = new URLSearchParams({ file_path: filePath });
|
||||
const response = await fetch(`${this.apiConfig.endpoints.modelDescription}?${params}`);
|
||||
|
||||
const response = await fetch(`${this.apiConfig.endpoints.modelDescription}?file_path=${encodeURIComponent(filePath)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} description: ${response.statusText}`);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
return data.description;
|
||||
} else {
|
||||
throw new Error(data.error || `No description found for ${this.apiConfig.config.singularName}`);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${this.apiConfig.config.singularName} description:`, error);
|
||||
console.error('Error fetching model description:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-organize models based on current path template settings
|
||||
* @param {Array} filePaths - Optional array of file paths to organize. If not provided, organizes all models.
|
||||
* @returns {Promise} - Promise that resolves when the operation is complete
|
||||
*/
|
||||
async autoOrganizeModels(filePaths = null) {
|
||||
let ws = null;
|
||||
|
||||
await state.loadingManager.showWithProgress(async (loading) => {
|
||||
try {
|
||||
// Connect to WebSocket for progress updates
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
||||
|
||||
const operationComplete = new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type !== 'auto_organize_progress') return;
|
||||
|
||||
switch(data.status) {
|
||||
case 'started':
|
||||
loading.setProgress(0);
|
||||
const operationType = data.operation_type === 'bulk' ? 'selected models' : 'all models';
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.starting', { type: operationType }, `Starting auto-organize for ${operationType}...`));
|
||||
break;
|
||||
|
||||
case 'processing':
|
||||
const percent = data.total > 0 ? ((data.processed / data.total) * 90).toFixed(1) : 0;
|
||||
loading.setProgress(percent);
|
||||
loading.setStatus(
|
||||
translate('loras.bulkOperations.autoOrganizeProgress.processing', {
|
||||
processed: data.processed,
|
||||
total: data.total,
|
||||
success: data.success,
|
||||
failures: data.failures,
|
||||
skipped: data.skipped
|
||||
}, `Processing (${data.processed}/${data.total}) - ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'cleaning':
|
||||
loading.setProgress(95);
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.cleaning', {}, 'Cleaning up empty directories...'));
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
loading.setProgress(100);
|
||||
loading.setStatus(
|
||||
translate('loras.bulkOperations.autoOrganizeProgress.completed', {
|
||||
success: data.success,
|
||||
skipped: data.skipped,
|
||||
failures: data.failures,
|
||||
total: data.total
|
||||
}, `Completed: ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(data);
|
||||
}, 1500);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.error', { error: data.error }, `Error: ${data.error}`));
|
||||
reject(new Error(data.error));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error during auto-organize:', error);
|
||||
reject(new Error('Connection error'));
|
||||
};
|
||||
});
|
||||
|
||||
// Start the auto-organize operation
|
||||
const endpoint = this.apiConfig.endpoints.autoOrganize;
|
||||
const requestOptions = {
|
||||
method: filePaths ? 'POST' : 'GET',
|
||||
headers: filePaths ? { 'Content-Type': 'application/json' } : {}
|
||||
};
|
||||
|
||||
if (filePaths) {
|
||||
requestOptions.body = JSON.stringify({ file_paths: filePaths });
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, requestOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to start auto-organize operation');
|
||||
}
|
||||
|
||||
// Wait for the operation to complete via WebSocket
|
||||
const result = await operationComplete;
|
||||
|
||||
// Show appropriate success message based on results
|
||||
if (result.failures === 0) {
|
||||
showToast('toast.loras.autoOrganizeSuccess', {
|
||||
count: result.success,
|
||||
type: result.operation_type === 'bulk' ? 'selected models' : 'all models'
|
||||
}, 'success');
|
||||
} else {
|
||||
showToast('toast.loras.autoOrganizePartialSuccess', {
|
||||
success: result.success,
|
||||
failures: result.failures,
|
||||
total: result.total
|
||||
}, 'warning');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during auto-organize:', error);
|
||||
showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialMessage: translate('loras.bulkOperations.autoOrganizeProgress.initializing', {}, 'Initializing auto-organize...'),
|
||||
completionMessage: translate('loras.bulkOperations.autoOrganizeProgress.complete', {}, 'Auto-organize complete')
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
|
||||
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||
|
||||
if (sendToWorkflowAppendItem) {
|
||||
@@ -50,6 +51,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
if (moveAllItem) {
|
||||
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
||||
}
|
||||
if (autoOrganizeItem) {
|
||||
autoOrganizeItem.style.display = config.autoOrganize ? 'flex' : 'none';
|
||||
}
|
||||
if (deleteAllItem) {
|
||||
deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none';
|
||||
}
|
||||
@@ -97,6 +101,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
case 'move-all':
|
||||
window.moveManager.showMoveModal('bulk');
|
||||
break;
|
||||
case 'auto-organize':
|
||||
bulkManager.autoOrganizeSelectedModels();
|
||||
break;
|
||||
case 'delete-all':
|
||||
bulkManager.showBulkDeleteModal();
|
||||
break;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||
import { PRESET_TAGS, BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||
import { eventManager } from '../utils/EventManager.js';
|
||||
@@ -34,6 +34,7 @@ export class BulkManager {
|
||||
copyAll: true,
|
||||
refreshAll: true,
|
||||
moveAll: true,
|
||||
autoOrganize: true,
|
||||
deleteAll: true
|
||||
},
|
||||
[MODEL_TYPES.EMBEDDING]: {
|
||||
@@ -42,6 +43,7 @@ export class BulkManager {
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
moveAll: true,
|
||||
autoOrganize: true,
|
||||
deleteAll: true
|
||||
},
|
||||
[MODEL_TYPES.CHECKPOINT]: {
|
||||
@@ -50,6 +52,7 @@ export class BulkManager {
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
moveAll: false,
|
||||
autoOrganize: true,
|
||||
deleteAll: true
|
||||
}
|
||||
};
|
||||
@@ -956,9 +959,48 @@ export class BulkManager {
|
||||
* Cleanup bulk base model modal
|
||||
*/
|
||||
cleanupBulkBaseModelModal() {
|
||||
const select = document.getElementById('bulkBaseModelSelect');
|
||||
if (select) {
|
||||
select.innerHTML = '';
|
||||
const modal = document.getElementById('bulkBaseModelModal');
|
||||
if (modal) {
|
||||
// Clear existing tags
|
||||
const tagsContainer = modal.querySelector('.bulk-tags');
|
||||
if (tagsContainer) {
|
||||
tagsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Clear dropdown
|
||||
const dropdown = modal.querySelector('.bulk-suggestions-dropdown');
|
||||
if (dropdown) {
|
||||
dropdown.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-organize selected models based on current path template settings
|
||||
*/
|
||||
async autoOrganizeSelectedModels() {
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.loras.noModelsSelected', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get selected file paths
|
||||
const filePaths = Array.from(state.selectedModels);
|
||||
|
||||
// Get the API client for the current model type
|
||||
const apiClient = getModelApiClient();
|
||||
|
||||
// Call the auto-organize method with selected file paths
|
||||
await apiClient.autoOrganizeModels(filePaths);
|
||||
|
||||
setTimeout(() => {
|
||||
resetAndReload(true);
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during bulk auto-organize:', error);
|
||||
showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
<div class="context-menu-item" data-action="move-all">
|
||||
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="auto-organize">
|
||||
<i class="fas fa-magic"></i> <span>{{ t('loras.bulkOperations.autoOrganize') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-separator"></div>
|
||||
<div class="context-menu-item delete-item" data-action="delete-all">
|
||||
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
||||
|
||||
Reference in New Issue
Block a user