feat: implement batch import recipe functionality (frontend + backend fixes)

Backend fixes:
- Add missing API route for /api/lm/recipes/batch-import/progress (GET)
- Add missing API route for /api/lm/recipes/batch-import/directory (POST)
- Add missing API route for /api/lm/recipes/browse-directory (POST)
- Register WebSocket endpoint for batch import progress
- Fix skip_no_metadata default value (True -> False) to allow no-LoRA imports
- Add items array to BatchImportProgress.to_dict() for detailed results

Frontend implementation:
- Create BatchImportManager.js with complete batch import workflow
- Add directory browser UI for selecting folders
- Add batch import modal with URL list and directory input modes
- Implement real-time progress tracking (WebSocket + HTTP polling)
- Add results summary with success/failed/skipped statistics
- Add expandable details view showing individual item status
- Auto-refresh recipe list after import completion

UI improvements:
- Add spinner animation for importing status
- Simplify results summary UI to match progress stats styling
- Fix current item text alignment
- Fix dark theme styling for directory browser button
- Fix batch import button styling consistency

Translations:
- Add batch import related i18n keys to all locale files
- Run sync_translation_keys.py to sync all translations

Fixes:
- Batch import now allows images without LoRAs (matches single import behavior)
- Progress endpoint now returns complete items array with status details
- Results view correctly displays skipped items with error messages
This commit is contained in:
Will Miao
2026-03-14 21:17:36 +08:00
parent f86651652c
commit ee466113d5
24 changed files with 2791 additions and 145 deletions

View File

@@ -69,7 +69,7 @@ class BatchImportProgress:
finished_at: Optional[float] = None
items: List[BatchImportItem] = field(default_factory=list)
tags: List[str] = field(default_factory=list)
skip_no_metadata: bool = True
skip_no_metadata: bool = False
skip_duplicates: bool = False
def to_dict(self) -> Dict[str, Any]:
@@ -87,6 +87,19 @@ class BatchImportProgress:
"progress_percent": round((self.completed / self.total) * 100, 1)
if self.total > 0
else 0,
"items": [
{
"id": item.id,
"source": item.source,
"item_type": item.item_type.value,
"status": item.status.value,
"error_message": item.error_message,
"recipe_name": item.recipe_name,
"recipe_id": item.recipe_id,
"duration": item.duration,
}
for item in self.items
],
}
@@ -226,7 +239,7 @@ class BatchImportService:
civitai_client_getter: Callable[[], Any],
items: List[Dict[str, str]],
tags: Optional[List[str]] = None,
skip_no_metadata: bool = True,
skip_no_metadata: bool = False,
skip_duplicates: bool = False,
) -> str:
operation_id = str(uuid.uuid4())
@@ -278,7 +291,7 @@ class BatchImportService:
directory: str,
recursive: bool = True,
tags: Optional[List[str]] = None,
skip_no_metadata: bool = True,
skip_no_metadata: bool = False,
skip_duplicates: bool = False,
) -> str:
image_paths = await self._discover_images(directory, recursive)
@@ -494,7 +507,8 @@ class BatchImportService:
"skipped": True,
"error": "No LoRAs found in image",
}
return {"success": False, "error": "No LoRAs found in image"}
# When skip_no_metadata is False, allow importing images without LoRAs
# Continue with empty loras list
recipe_name = self._generate_recipe_name(item, payload)
all_tags = list(set(tags + (payload.get("tags", []) or [])))