fix: improve example image upload reliability and error handling, #804

- Sequential per-file upload to avoid client_max_size limits
- Add backend exception handler with proper 500 responses
- Increase standalone server upload limit to 256MB
- Add partial success localization support
This commit is contained in:
Will Miao
2026-02-08 09:16:48 +08:00
parent 199c9f742c
commit b4ad03c9bf
13 changed files with 63 additions and 27 deletions

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Fehler beim Laden des Ordnerbaums",
"folderTreeError": "Fehler beim Laden des Ordnerbaums",
"imagesImported": "Beispielbilder erfolgreich importiert",
"imagesPartial": "{success} Bild(er) importiert, {failed} fehlgeschlagen",
"importFailed": "Fehler beim Importieren der Beispielbilder: {message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Failed to load folder tree",
"folderTreeError": "Error loading folder tree",
"imagesImported": "Example images imported successfully",
"imagesPartial": "{success} image(s) imported, {failed} failed",
"importFailed": "Failed to import example images: {message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Error al cargar árbol de carpetas",
"folderTreeError": "Error al cargar árbol de carpetas",
"imagesImported": "Imágenes de ejemplo importadas exitosamente",
"imagesPartial": "{success} imagen(es) importada(s), {failed} fallida(s)",
"importFailed": "Error al importar imágenes de ejemplo: {message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Échec du chargement de l'arborescence des dossiers",
"folderTreeError": "Erreur lors du chargement de l'arborescence des dossiers",
"imagesImported": "Images d'exemple importées avec succès",
"imagesPartial": "{success} image(s) importée(s), {failed} échouée(s)",
"importFailed": "Échec de l'importation des images d'exemple : {message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "טעינת עץ התיקיות נכשלה",
"folderTreeError": "שגיאה בטעינת עץ התיקיות",
"imagesImported": "תמונות הדוגמה יובאו בהצלחה",
"imagesPartial": "{success} תמונה/ות יובאו, {failed} נכשלו",
"importFailed": "ייבוא תמונות הדוגמה נכשל: {message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "フォルダツリーの読み込みに失敗しました",
"folderTreeError": "フォルダツリー読み込みエラー",
"imagesImported": "例画像が正常にインポートされました",
"imagesPartial": "{success} 件の画像をインポート、{failed} 件失敗",
"importFailed": "例画像のインポートに失敗しました:{message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "폴더 트리 로딩 실패",
"folderTreeError": "폴더 트리 로딩 오류",
"imagesImported": "예시 이미지가 성공적으로 가져와졌습니다",
"imagesPartial": "{success}개 이미지 가져오기 성공, {failed}개 실패",
"importFailed": "예시 이미지 가져오기 실패: {message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Не удалось загрузить дерево папок",
"folderTreeError": "Ошибка загрузки дерева папок",
"imagesImported": "Примеры изображений успешно импортированы",
"imagesPartial": "{success} изображ. импортировано, {failed} не удалось",
"importFailed": "Не удалось импортировать примеры изображений: {message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "加载文件夹树失败",
"folderTreeError": "加载文件夹树出错",
"imagesImported": "示例图片导入成功",
"imagesPartial": "成功导入 {success} 张图片,{failed} 张失败",
"importFailed": "导入示例图片失败:{message}"
},
"triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "載入資料夾樹狀結構失敗",
"folderTreeError": "載入資料夾樹狀結構錯誤",
"imagesImported": "範例圖片匯入成功",
"imagesPartial": "成功匯入 {success} 張圖片,{failed} 張失敗",
"importFailed": "匯入範例圖片失敗:{message}"
},
"triggerWords": {

View File

@@ -1,11 +1,14 @@
"""Handler set for example image routes."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Callable, Mapping
from aiohttp import web
logger = logging.getLogger(__name__)
from ...services.use_cases.example_images import (
DownloadExampleImagesConfigurationError,
DownloadExampleImagesInProgressError,
@@ -122,6 +125,9 @@ class ExampleImagesManagementHandler:
return web.json_response({'success': False, 'error': str(exc)}, status=400)
except ExampleImagesImportError as exc:
return web.json_response({'success': False, 'error': str(exc)}, status=500)
except Exception as exc:
logger.exception("Unexpected error importing example images")
return web.json_response({'success': False, 'error': str(exc)}, status=500)
async def delete_example_image(self, request: web.Request) -> web.StreamResponse:
return await self._processor.delete_custom_image(request)

View File

@@ -154,6 +154,7 @@ class StandaloneServer:
self.app = web.Application(
logger=logger,
middlewares=[cache_control],
client_max_size=256 * 1024 * 1024,
handler_args={
"max_field_size": HEADER_SIZE_LIMIT,
"max_line_size": HEADER_SIZE_LIMIT,

View File

@@ -455,34 +455,49 @@ async function handleImportFiles(files, modelHash, importContainer) {
}
try {
// Use FormData to upload files
const formData = new FormData();
formData.append('model_hash', modelHash);
validFiles.forEach(file => {
formData.append('files', file);
});
// Call API to import files
const response = await fetch('/api/lm/import-example-images', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Failed to import example files');
// Upload files one at a time to avoid exceeding server size limits
let lastSuccessResult = null;
let successCount = 0;
const errors = [];
for (const file of validFiles) {
try {
const formData = new FormData();
formData.append('model_hash', modelHash);
formData.append('files', file);
const response = await fetch('/api/lm/import-example-images', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
errors.push(`${file.name}: ${result.error || 'Unknown error'}`);
} else {
lastSuccessResult = result;
successCount++;
}
} catch (err) {
errors.push(`${file.name}: ${err.message}`);
}
}
if (successCount === 0) {
throw new Error(errors.join('; '));
}
const result = lastSuccessResult;
// Get updated local files
const updatedFilesResponse = await fetch(`/api/lm/example-image-files?model_hash=${modelHash}`);
const updatedFilesResult = await updatedFilesResponse.json();
if (!updatedFilesResult.success) {
throw new Error(updatedFilesResult.error || 'Failed to get updated file list');
}
// Re-render the showcase content
const showcaseTab = document.getElementById('showcase-tab');
if (showcaseTab) {
@@ -492,18 +507,22 @@ async function handleImportFiles(files, modelHash, importContainer) {
// Combine both arrays for rendering
const allImages = [...regularImages, ...customImages];
showcaseTab.innerHTML = renderShowcaseContent(allImages, updatedFilesResult.files, true);
// Re-initialize showcase functionality
const carousel = showcaseTab.querySelector('.carousel');
if (carousel && !carousel.classList.contains('collapsed')) {
initShowcaseContent(carousel);
}
// Initialize the import UI for the new content
initExampleImport(modelHash, showcaseTab);
showToast('toast.import.imagesImported', {}, 'success');
if (errors.length > 0) {
showToast('toast.import.imagesPartial', { success: successCount, failed: errors.length }, 'warning');
} else {
showToast('toast.import.imagesImported', {}, 'success');
}
// Update VirtualScroller if available
if (state.virtualScroller && result.model_file_path) {
// Create an update object with only the necessary properties
@@ -513,7 +532,7 @@ async function handleImportFiles(files, modelHash, importContainer) {
customImages: customImages
}
};
// Update the item in the virtual scroller
state.virtualScroller.updateSingleItem(result.model_file_path, updateData);
}