diff --git a/src/core/batch.py b/src/core/batch.py index 7de39c3..606fea2 100644 --- a/src/core/batch.py +++ b/src/core/batch.py @@ -1,2 +1,138 @@ -def run_batch(*args, **kwargs): - raise NotImplementedError("Batch composition is not implemented yet.") +import logging +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, + ) diff --git a/tasks.md b/tasks.md index 35dfa8c..0fe40bc 100644 --- a/tasks.md +++ b/tasks.md @@ -238,21 +238,21 @@ 任务: -- [ ] 先读取 `src/core/batch.py` 现有内容 -- [ ] 完善 `src/core/batch.py` -- [ ] 支持多衣服 × 单印花 -- [ ] 支持单衣服 × 多印花 -- [ ] 支持一一匹配 -- [ ] 支持全组合 -- [ ] 复用 `core/composer.py` -- [ ] 单个任务失败时继续处理剩余任务 -- [ ] 汇总成功数量、失败数量和失败原因 +- [x] 先读取 `src/core/batch.py` 现有内容 +- [x] 完善 `src/core/batch.py` +- [x] 支持多衣服 × 单印花 +- [x] 支持单衣服 × 多印花 +- [x] 支持一一匹配 +- [x] 支持全组合 +- [x] 复用 `core/composer.py` +- [x] 单个任务失败时继续处理剩余任务 +- [x] 汇总成功数量、失败数量和失败原因 验收: -- [ ] 不复制第二套合成算法 -- [ ] 单张失败不会中断整个批量任务 -- [ ] 失败原因可供 UI 和日志使用 +- [x] 不复制第二套合成算法 +- [x] 单张失败不会中断整个批量任务 +- [x] 失败原因可供 UI 和日志使用 ## 7. 主界面布局