diff --git a/locales/de.json b/locales/de.json index 7803b50d..d73a5df8 100644 --- a/locales/de.json +++ b/locales/de.json @@ -689,6 +689,7 @@ "setContentRating": "Inhaltsbewertung für alle festlegen", "copyAll": "Alle Syntax kopieren", "refreshAll": "Alle Metadaten aktualisieren", + "repairMetadata": "Metadaten der Auswahl reparieren", "checkUpdates": "Auswahl auf Updates prüfen", "moveAll": "Alle in Ordner verschieben", "autoOrganize": "Automatisch organisieren", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "Keine Rezepte ausgewählt", + "repairBulkComplete": "Reparatur abgeschlossen: {repaired} repariert, {skipped} übersprungen (von {total})", + "repairBulkSkipped": "Keine Reparatur für die {total} ausgewählten Rezepte erforderlich", + "repairBulkFailed": "Reparatur der ausgewählten Rezepte fehlgeschlagen: {message}", "noMissingLorasInSelection": "Keine fehlenden LoRAs in ausgewählten Rezepten gefunden", "noLoraRootConfigured": "Kein LoRA-Stammverzeichnis konfiguriert. Bitte legen Sie ein Standard-LoRA-Stammverzeichnis in den Einstellungen fest." }, diff --git a/locales/en.json b/locales/en.json index ab8696a9..b9bc05ba 100644 --- a/locales/en.json +++ b/locales/en.json @@ -689,6 +689,7 @@ "setContentRating": "Set Content Rating for Selected", "copyAll": "Copy Selected Syntax", "refreshAll": "Refresh Selected Metadata", + "repairMetadata": "Repair Metadata for Selected", "checkUpdates": "Check Updates for Selected", "moveAll": "Move Selected to Folder", "autoOrganize": "Auto-Organize Selected", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "No recipes selected", + "repairBulkComplete": "Repair complete: {repaired} repaired, {skipped} skipped (of {total})", + "repairBulkSkipped": "No repair needed for any of the {total} selected recipes", + "repairBulkFailed": "Failed to repair selected recipes: {message}", "noMissingLorasInSelection": "No missing LoRAs found in selected recipes", "noLoraRootConfigured": "No LoRA root directory configured. Please set a default LoRA root in settings." }, diff --git a/locales/es.json b/locales/es.json index 154f1d71..c5b7cde0 100644 --- a/locales/es.json +++ b/locales/es.json @@ -689,6 +689,7 @@ "setContentRating": "Establecer clasificación de contenido para todos", "copyAll": "Copiar toda la sintaxis", "refreshAll": "Actualizar todos los metadatos", + "repairMetadata": "Reparar metadatos de la selección", "checkUpdates": "Comprobar actualizaciones para la selección", "moveAll": "Mover todos a carpeta", "autoOrganize": "Auto-organizar seleccionados", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "No se han seleccionado recetas", + "repairBulkComplete": "Reparación completa: {repaired} reparadas, {skipped} omitidas (de {total})", + "repairBulkSkipped": "No se necesita reparación para ninguna de las {total} recetas seleccionadas", + "repairBulkFailed": "Error al reparar las recetas seleccionadas: {message}", "noMissingLorasInSelection": "No se encontraron LoRAs faltantes en las recetas seleccionadas", "noLoraRootConfigured": "No se ha configurado el directorio raíz de LoRA. Por favor, establezca un directorio raíz de LoRA predeterminado en la configuración." }, diff --git a/locales/fr.json b/locales/fr.json index 75a299ab..60c129ef 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -689,6 +689,7 @@ "setContentRating": "Définir la classification du contenu pour tous", "copyAll": "Copier toute la syntaxe", "refreshAll": "Actualiser toutes les métadonnées", + "repairMetadata": "Réparer les métadonnées de la sélection", "checkUpdates": "Vérifier les mises à jour pour la sélection", "moveAll": "Déplacer tout vers un dossier", "autoOrganize": "Auto-organiser la sélection", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "Aucune recette sélectionnée", + "repairBulkComplete": "Réparation terminée : {repaired} réparée(s), {skipped} ignorée(s) (sur {total})", + "repairBulkSkipped": "Aucune réparation nécessaire parmi les {total} recettes sélectionnées", + "repairBulkFailed": "Échec de la réparation des recettes sélectionnées : {message}", "noMissingLorasInSelection": "Aucun LoRA manquant trouvé dans les recettes sélectionnées", "noLoraRootConfigured": "Aucun répertoire racine LoRA configuré. Veuillez définir un répertoire racine LoRA par défaut dans les paramètres." }, diff --git a/locales/he.json b/locales/he.json index b8500d18..f9088b8f 100644 --- a/locales/he.json +++ b/locales/he.json @@ -689,6 +689,7 @@ "setContentRating": "הגדר דירוג תוכן לכל המודלים", "copyAll": "העתק את כל התחבירים", "refreshAll": "רענן את כל המטא-דאטה", + "repairMetadata": "תקן מטא-דאטה עבור הנבחרים", "checkUpdates": "בדוק עדכונים לבחירה", "moveAll": "העבר הכל לתיקייה", "autoOrganize": "ארגן אוטומטית נבחרים", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "לא נבחרו מתכונים", + "repairBulkComplete": "התיקון הושלם: {repaired} תוקנו, {skipped} דולגו (מתוך {total})", + "repairBulkSkipped": "אין צורך בתיקון עבור {total} המתכונים הנבחרים", + "repairBulkFailed": "תיקון המתכונים הנבחרים נכשל: {message}", "noMissingLorasInSelection": "לא נמצאו LoRAs חסרים במתכונים שנבחרו", "noLoraRootConfigured": "תיקיית השורש של LoRA לא מוגדרת. אנא הגדר תיקיית שורש LoRA ברירת מחדל בהגדרות." }, diff --git a/locales/ja.json b/locales/ja.json index bbbfa5a2..677fe9db 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -689,6 +689,7 @@ "setContentRating": "すべてのモデルのコンテンツレーティングを設定", "copyAll": "すべての構文をコピー", "refreshAll": "すべてのメタデータを更新", + "repairMetadata": "選択したレシピのメタデータを修復", "checkUpdates": "選択項目の更新を確認", "moveAll": "すべてをフォルダに移動", "autoOrganize": "自動整理を実行", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "レシピが選択されていません", + "repairBulkComplete": "修復完了:{repaired} 件修復、{skipped} 件スキップ(合計 {total} 件)", + "repairBulkSkipped": "選択した {total} 件のレシピは修復不要です", + "repairBulkFailed": "選択したレシピの修復に失敗しました:{message}", "noMissingLorasInSelection": "選択したレシピに不足している LoRA が見つかりませんでした", "noLoraRootConfigured": "LoRA ルートディレクトリが設定されていません。設定でデフォルトの LoRA ルートを設定してください。" }, diff --git a/locales/ko.json b/locales/ko.json index 26e93c9b..502b668a 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -689,6 +689,7 @@ "setContentRating": "모든 모델에 콘텐츠 등급 설정", "copyAll": "모든 문법 복사", "refreshAll": "모든 메타데이터 새로고침", + "repairMetadata": "선택한 레시피 메타데이터 복구", "checkUpdates": "선택 항목 업데이트 확인", "moveAll": "모두 폴더로 이동", "autoOrganize": "자동 정리 선택", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "선택한 레시피가 없습니다", + "repairBulkComplete": "복구 완료: {repaired}개 복구, {skipped}개 건너뜀 (총 {total}개)", + "repairBulkSkipped": "선택한 {total}개 레시피는 복구가 필요하지 않습니다", + "repairBulkFailed": "선택한 레시피 복구 실패: {message}", "noMissingLorasInSelection": "선택한 레시피에서 누락된 LoRA를 찾을 수 없습니다", "noLoraRootConfigured": "LoRA 루트 디렉토리가 구성되지 않았습니다. 설정에서 기본 LoRA 루트를 설정하세요." }, diff --git a/locales/ru.json b/locales/ru.json index c4e9e062..f8b346d0 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -689,6 +689,7 @@ "setContentRating": "Установить рейтинг контента для всех", "copyAll": "Копировать весь синтаксис", "refreshAll": "Обновить все метаданные", + "repairMetadata": "Восстановить метаданные для выбранных", "checkUpdates": "Проверить обновления для выбранных", "moveAll": "Переместить все в папку", "autoOrganize": "Автоматически организовать выбранные", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "Рецепты не выбраны", + "repairBulkComplete": "Восстановление завершено: {repaired} восстановлено, {skipped} пропущено (из {total})", + "repairBulkSkipped": "Ни один из {total} выбранных рецептов не требует восстановления", + "repairBulkFailed": "Не удалось восстановить выбранные рецепты: {message}", "noMissingLorasInSelection": "В выбранных рецептах не найдены отсутствующие LoRAs", "noLoraRootConfigured": "Корневой каталог LoRA не настроен. Пожалуйста, установите корневой каталог LoRA по умолчанию в настройках." }, diff --git a/locales/zh-CN.json b/locales/zh-CN.json index a383d49e..36e6f2d1 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -689,6 +689,7 @@ "setContentRating": "为所选中设置内容评级", "copyAll": "复制所选中语法", "refreshAll": "刷新所选中元数据", + "repairMetadata": "修复所选中元数据", "checkUpdates": "检查所选更新", "moveAll": "移动所选中到文件夹", "autoOrganize": "自动整理所选模型", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "浏览目录失败:{message}", "batchImportDirectorySelected": "已选择目录:{path}", "noRecipesSelected": "未选择任何配方", + "repairBulkComplete": "修复完成:{repaired} 个已修复,{skipped} 个已跳过(共 {total} 个)", + "repairBulkSkipped": "所选 {total} 个配方无需修复", + "repairBulkFailed": "修复所选配方失败:{message}", "noMissingLorasInSelection": "在选定的配方中未找到缺失的 LoRAs", "noLoraRootConfigured": "未配置 LoRA 根目录。请在设置中设置默认的 LoRA 根目录。" }, diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 2f3271fd..b2cd78ef 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -689,6 +689,7 @@ "setContentRating": "為全部設定內容分級", "copyAll": "複製全部語法", "refreshAll": "刷新全部 metadata", + "repairMetadata": "修復所選中元數據", "checkUpdates": "檢查所選更新", "moveAll": "全部移動到資料夾", "autoOrganize": "自動整理所選模型", @@ -1693,6 +1694,9 @@ "batchImportBrowseFailed": "瀏覽目錄失敗:{message}", "batchImportDirectorySelected": "已選擇目錄:{path}", "noRecipesSelected": "未選取任何食譜", + "repairBulkComplete": "修復完成:{repaired} 個已修復,{skipped} 個已跳過(共 {total} 個)", + "repairBulkSkipped": "所選 {total} 個配方無需修復", + "repairBulkFailed": "修復所選配方失敗:{message}", "noMissingLorasInSelection": "在選取的食譜中未找到缺失的 LoRAs", "noLoraRootConfigured": "未配置 LoRA 根目錄。請在設定中設定預設的 LoRA 根目錄。" }, diff --git a/py/routes/handlers/recipe_handlers.py b/py/routes/handlers/recipe_handlers.py index 1bee87ac..413b6eab 100644 --- a/py/routes/handlers/recipe_handlers.py +++ b/py/routes/handlers/recipe_handlers.py @@ -87,6 +87,7 @@ class RecipeHandlerSet: "repair_recipes": self.management.repair_recipes, "cancel_repair": self.management.cancel_repair, "repair_recipe": self.management.repair_recipe, + "repair_recipes_bulk": self.management.repair_recipes_bulk, "get_repair_progress": self.management.get_repair_progress, "start_batch_import": self.batch_import.start_batch_import, "get_batch_import_progress": self.batch_import.get_batch_import_progress, @@ -706,6 +707,69 @@ class RecipeManagementHandler: self._logger.error("Error cancelling recipe repair: %s", exc, exc_info=True) return web.json_response({"success": False, "error": str(exc)}, status=500) + async def repair_recipes_bulk(self, request: web.Request) -> web.Response: + """Bulk repair metadata for multiple recipes by their IDs. + + Accepts a JSON body with a "recipe_ids" array and iterates + repair_recipe_by_id over each entry, collecting statistics. + """ + try: + await self._ensure_dependencies_ready() + recipe_scanner = self._recipe_scanner_getter() + if recipe_scanner is None: + return web.json_response( + {"success": False, "error": "Recipe scanner unavailable"}, + status=503, + ) + + data = await request.json() + recipe_ids = data.get("recipe_ids", []) + if not recipe_ids: + return web.json_response( + {"success": False, "error": "recipe_ids are required"}, + status=400, + ) + + total = len(recipe_ids) + repaired = 0 + skipped = 0 + errors = 0 + recipes = [] + + for recipe_id in recipe_ids: + try: + result = await recipe_scanner.repair_recipe_by_id(recipe_id) + if result.get("success"): + repaired += result.get("repaired", 0) + skipped += result.get("skipped", 0) + if result.get("recipe"): + recipes.append(result["recipe"]) + else: + errors += 1 + except RecipeNotFoundError: + skipped += 1 + except Exception as exc: + self._logger.error( + "Error repairing recipe %s: %s", recipe_id, exc + ) + errors += 1 + + return web.json_response({ + "success": True, + "total": total, + "repaired": repaired, + "skipped": skipped, + "errors": errors, + "recipes": recipes, + }) + except Exception as exc: + self._logger.error( + "Error performing bulk repair: %s", exc, exc_info=True + ) + return web.json_response( + {"success": False, "error": str(exc)}, status=500 + ) + async def repair_recipe(self, request: web.Request) -> web.Response: try: await self._ensure_dependencies_ready() diff --git a/py/routes/recipe_route_registrar.py b/py/routes/recipe_route_registrar.py index b4c4bf05..4b2a262f 100644 --- a/py/routes/recipe_route_registrar.py +++ b/py/routes/recipe_route_registrar.py @@ -58,6 +58,7 @@ ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = ( RouteDefinition("POST", "/api/lm/recipes/repair", "repair_recipes"), RouteDefinition("POST", "/api/lm/recipes/cancel-repair", "cancel_repair"), RouteDefinition("POST", "/api/lm/recipe/{recipe_id}/repair", "repair_recipe"), + RouteDefinition("POST", "/api/lm/recipes/repair-bulk", "repair_recipes_bulk"), RouteDefinition("GET", "/api/lm/recipes/repair-progress", "get_repair_progress"), RouteDefinition("POST", "/api/lm/recipes/batch-import/start", "start_batch_import"), RouteDefinition( diff --git a/static/js/api/recipeApi.js b/static/js/api/recipeApi.js index ea974d36..d7df465c 100644 --- a/static/js/api/recipeApi.js +++ b/static/js/api/recipeApi.js @@ -15,6 +15,7 @@ const RECIPE_ENDPOINTS = { move: '/api/lm/recipe/move', moveBulk: '/api/lm/recipes/move-bulk', bulkDelete: '/api/lm/recipes/bulk-delete', + repairBulk: '/api/lm/recipes/repair-bulk', }; const RECIPE_SIDEBAR_CONFIG = { @@ -557,6 +558,38 @@ export class RecipeSidebarApiClient { }; } + async repairBulkModels(filePaths) { + if (!filePaths || filePaths.length === 0) { + throw new Error('No file paths provided'); + } + + const recipeIds = filePaths + .map((path) => extractRecipeId(path)) + .filter((id) => !!id); + + if (recipeIds.length === 0) { + throw new Error('No recipe IDs could be derived from file paths'); + } + + const response = await fetch(this.apiConfig.endpoints.repairBulk, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + recipe_ids: recipeIds, + }), + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + throw new Error(result.error || 'Failed to repair recipes'); + } + + return result; + } + async bulkDeleteModels(filePaths) { if (!filePaths || filePaths.length === 0) { throw new Error('No file paths provided'); diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js index 4ad0dbe2..b076a2b3 100644 --- a/static/js/components/ContextMenu/BulkContextMenu.js +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -41,6 +41,11 @@ export class BulkContextMenu extends BaseContextMenu { const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]'); const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]'); const downloadMissingLorasItem = this.menu.querySelector('[data-action="download-missing-loras"]'); + const repairMetadataItem = this.menu.querySelector('[data-action="repair-metadata"]'); + + if (repairMetadataItem) { + repairMetadataItem.style.display = config.repairMetadata ? 'flex' : 'none'; + } if (sendToWorkflowAppendItem) { sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none'; @@ -256,6 +261,9 @@ export class BulkContextMenu extends BaseContextMenu { case 'delete-all': bulkManager.showBulkDeleteModal(); break; + case 'repair-metadata': + bulkManager.repairSelectedRecipes(); + break; case 'set-favorite': { const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size; bulkManager.setBulkFavorites(!allFavorited); diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 3366dda0..488f7a4e 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -85,7 +85,8 @@ export class BulkManager { setContentRating: false, skipMetadataRefresh: false, setFavorite: true, - unfavorite: true + unfavorite: true, + repairMetadata: true } }; @@ -656,6 +657,76 @@ export class BulkManager { } } + async repairSelectedRecipes() { + if (state.selectedModels.size === 0) { + showToast('toast.recipes.noRecipesSelected', {}, 'warning'); + return; + } + + if (state.currentPageType !== 'recipes') { + showToast('This operation is only available for recipes', {}, 'warning'); + return; + } + + try { + const apiClient = this.getActiveApiClient(); + const filePaths = Array.from(state.selectedModels); + + if (typeof apiClient.repairBulkModels !== 'function') { + showToast('Bulk repair is not supported for this model type', {}, 'error'); + return; + } + + state.loadingManager.showSimpleLoading('Repairing recipe metadata...'); + + const result = await apiClient.repairBulkModels(filePaths); + + if (result.success) { + const total = result.total || filePaths.length; + const repaired = result.repaired || 0; + const skipped = result.skipped || 0; + + const recipes = result.recipes || []; + for (const recipe of recipes) { + if (recipe.file_path) { + state.virtualScroller.updateSingleItem( + recipe.file_path, + recipe + ); + } + } + + if (repaired > 0) { + showToast( + 'toast.recipes.repairBulkComplete', + { repaired, skipped, total }, + 'success' + ); + } else { + showToast( + 'toast.recipes.repairBulkSkipped', + { total }, + 'info' + ); + } + + this.clearSelection(); + } else { + throw new Error(result.error || 'Bulk repair failed'); + } + } catch (error) { + console.error('Error during bulk recipe repair:', error); + showToast('toast.recipes.repairBulkFailed', { message: error.message }, 'error'); + } finally { + if (state.loadingManager?.hide) { + state.loadingManager.hide(); + } + if (typeof state.loadingManager?.restoreProgressBar === 'function') { + state.loadingManager.restoreProgressBar(); + } + } + } + async refreshAllMetadata() { if (state.selectedModels.size === 0) { showToast('toast.models.noModelsSelected', {}, 'warning'); diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index 74d916ad..28bb8a5d 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -80,6 +80,9 @@
+