feat: implement batch composition core
This commit is contained in:
+138
-2
@@ -1,2 +1,138 @@
|
|||||||
def run_batch(*args, **kwargs):
|
import logging
|
||||||
raise NotImplementedError("Batch composition is not implemented yet.")
|
from pathlib import Path
|
||||||
|
from typing import Callable, Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from core.composer import compose
|
||||||
|
from core.models import (
|
||||||
|
BatchMode,
|
||||||
|
BatchOptions,
|
||||||
|
BatchResult,
|
||||||
|
ComposeResult,
|
||||||
|
ImageAsset,
|
||||||
|
TransformState,
|
||||||
|
)
|
||||||
|
from services.file_service import get_output_dir, make_safe_output_path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PathLike = Union[str, Path]
|
||||||
|
ProgressCallback = Callable[[int, int, ComposeResult], None]
|
||||||
|
|
||||||
|
|
||||||
|
def run_batch(
|
||||||
|
garments: Iterable[Union[ImageAsset, PathLike]],
|
||||||
|
prints: Iterable[Union[ImageAsset, PathLike]],
|
||||||
|
transform: TransformState,
|
||||||
|
options: BatchOptions,
|
||||||
|
progress_callback: Optional[ProgressCallback] = None,
|
||||||
|
) -> BatchResult:
|
||||||
|
"""Run batch composition tasks.
|
||||||
|
|
||||||
|
The function only orchestrates pair generation and delegates all image
|
||||||
|
processing to core.composer.compose(). A failed item is recorded and does
|
||||||
|
not stop later tasks.
|
||||||
|
"""
|
||||||
|
garment_paths = _selected_paths(garments)
|
||||||
|
print_paths = _selected_paths(prints)
|
||||||
|
pairs = _build_pairs(garment_paths, print_paths, options.mode)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
export_options = options.export_options
|
||||||
|
output_dir = Path(export_options.output_dir) if export_options.output_dir else get_output_dir()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Batch started: mode=%s, garments=%d, prints=%d, tasks=%d",
|
||||||
|
options.mode.value,
|
||||||
|
len(garment_paths),
|
||||||
|
len(print_paths),
|
||||||
|
len(pairs),
|
||||||
|
)
|
||||||
|
|
||||||
|
total = len(pairs)
|
||||||
|
for index, (garment_path, print_path) in enumerate(pairs, start=1):
|
||||||
|
try:
|
||||||
|
output_path = make_safe_output_path(
|
||||||
|
output_dir,
|
||||||
|
garment_path,
|
||||||
|
print_path,
|
||||||
|
export_options.output_format,
|
||||||
|
)
|
||||||
|
result = compose(
|
||||||
|
garment_path=garment_path,
|
||||||
|
print_path=print_path,
|
||||||
|
transform=transform,
|
||||||
|
options=export_options,
|
||||||
|
output_path=output_path,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Batch item failed unexpectedly: %s + %s", garment_path, print_path)
|
||||||
|
result = ComposeResult(
|
||||||
|
success=False,
|
||||||
|
garment_path=garment_path,
|
||||||
|
print_path=print_path,
|
||||||
|
error=str(exc),
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
if progress_callback is not None:
|
||||||
|
progress_callback(index, total, result)
|
||||||
|
|
||||||
|
batch_result = _summarize(results)
|
||||||
|
logger.info(
|
||||||
|
"Batch finished: total=%d, success=%d, failure=%d",
|
||||||
|
batch_result.total,
|
||||||
|
batch_result.success_count,
|
||||||
|
batch_result.failure_count,
|
||||||
|
)
|
||||||
|
return batch_result
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_paths(items: Iterable[Union[ImageAsset, PathLike]]) -> List[Path]:
|
||||||
|
paths = []
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, ImageAsset):
|
||||||
|
if not item.selected:
|
||||||
|
continue
|
||||||
|
paths.append(Path(item.path))
|
||||||
|
else:
|
||||||
|
paths.append(Path(item))
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def _build_pairs(
|
||||||
|
garment_paths: List[Path],
|
||||||
|
print_paths: List[Path],
|
||||||
|
mode: BatchMode,
|
||||||
|
) -> List[Tuple[Path, Path]]:
|
||||||
|
if mode == BatchMode.MANY_GARMENTS:
|
||||||
|
if not print_paths:
|
||||||
|
return []
|
||||||
|
return [(garment_path, print_paths[0]) for garment_path in garment_paths]
|
||||||
|
|
||||||
|
if mode == BatchMode.MANY_PRINTS:
|
||||||
|
if not garment_paths:
|
||||||
|
return []
|
||||||
|
return [(garment_paths[0], print_path) for print_path in print_paths]
|
||||||
|
|
||||||
|
if mode == BatchMode.ONE_TO_ONE:
|
||||||
|
return list(zip(garment_paths, print_paths))
|
||||||
|
|
||||||
|
if mode == BatchMode.FULL_COMBO:
|
||||||
|
return [
|
||||||
|
(garment_path, print_path)
|
||||||
|
for garment_path in garment_paths
|
||||||
|
for print_path in print_paths
|
||||||
|
]
|
||||||
|
|
||||||
|
raise ValueError("Unsupported batch mode: {}".format(mode))
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize(results: List[ComposeResult]) -> BatchResult:
|
||||||
|
success_count = sum(1 for result in results if result.success)
|
||||||
|
failure_count = len(results) - success_count
|
||||||
|
return BatchResult(
|
||||||
|
results=results,
|
||||||
|
total=len(results),
|
||||||
|
success_count=success_count,
|
||||||
|
failure_count=failure_count,
|
||||||
|
)
|
||||||
|
|||||||
@@ -238,21 +238,21 @@
|
|||||||
|
|
||||||
任务:
|
任务:
|
||||||
|
|
||||||
- [ ] 先读取 `src/core/batch.py` 现有内容
|
- [x] 先读取 `src/core/batch.py` 现有内容
|
||||||
- [ ] 完善 `src/core/batch.py`
|
- [x] 完善 `src/core/batch.py`
|
||||||
- [ ] 支持多衣服 × 单印花
|
- [x] 支持多衣服 × 单印花
|
||||||
- [ ] 支持单衣服 × 多印花
|
- [x] 支持单衣服 × 多印花
|
||||||
- [ ] 支持一一匹配
|
- [x] 支持一一匹配
|
||||||
- [ ] 支持全组合
|
- [x] 支持全组合
|
||||||
- [ ] 复用 `core/composer.py`
|
- [x] 复用 `core/composer.py`
|
||||||
- [ ] 单个任务失败时继续处理剩余任务
|
- [x] 单个任务失败时继续处理剩余任务
|
||||||
- [ ] 汇总成功数量、失败数量和失败原因
|
- [x] 汇总成功数量、失败数量和失败原因
|
||||||
|
|
||||||
验收:
|
验收:
|
||||||
|
|
||||||
- [ ] 不复制第二套合成算法
|
- [x] 不复制第二套合成算法
|
||||||
- [ ] 单张失败不会中断整个批量任务
|
- [x] 单张失败不会中断整个批量任务
|
||||||
- [ ] 失败原因可供 UI 和日志使用
|
- [x] 失败原因可供 UI 和日志使用
|
||||||
|
|
||||||
## 7. 主界面布局
|
## 7. 主界面布局
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user