2026-06-15 16:56:36 +08:00
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
from enum import Enum
|
2026-06-15 15:53:01 +08:00
|
|
|
|
from pathlib import Path
|
2026-06-15 16:56:36 +08:00
|
|
|
|
from typing import List, Optional
|
2026-06-15 15:53:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 16:56:36 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 素材
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-06-15 15:53:01 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class ImageAsset:
|
2026-06-15 16:56:36 +08:00
|
|
|
|
"""一张图片素材(衣服图或印花图)。"""
|
2026-06-15 15:53:01 +08:00
|
|
|
|
path: Path
|
2026-06-15 16:56:36 +08:00
|
|
|
|
selected: bool = True # 是否参与批量合成
|
2026-06-15 15:53:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 16:56:36 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 印花变换状态
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-06-15 15:53:01 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class TransformState:
|
2026-06-15 16:56:36 +08:00
|
|
|
|
"""印花在衣服原图像素坐标系中的变换参数。
|
|
|
|
|
|
|
|
|
|
|
|
x/y: 未旋转包围盒左上角坐标(原图像素)。
|
|
|
|
|
|
width/height: 缩放后尺寸(原图像素)。
|
|
|
|
|
|
rotation: 围绕印花中心旋转角度(度)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
x: float = 0.0
|
|
|
|
|
|
y: float = 0.0
|
|
|
|
|
|
width: float = 0.0
|
|
|
|
|
|
height: float = 0.0
|
|
|
|
|
|
rotation: float = 0.0
|
2026-06-15 15:53:01 +08:00
|
|
|
|
keep_aspect_ratio: bool = True
|
2026-06-15 16:56:36 +08:00
|
|
|
|
|
|
|
|
|
|
def center(self):
|
|
|
|
|
|
"""返回印花中心坐标 (cx, cy)。"""
|
|
|
|
|
|
return self.x + self.width / 2.0, self.y + self.height / 2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 模板
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class Template:
|
|
|
|
|
|
"""可复用的合成参数模板,使用比例坐标适配任意尺寸衣服图。
|
|
|
|
|
|
|
|
|
|
|
|
x_ratio / y_ratio: 印花左上角相对于衣服宽高的比例。
|
|
|
|
|
|
width_ratio / height_ratio: 印花宽高相对于衣服宽高的比例。
|
|
|
|
|
|
type: "builtin" 为内置模板,"custom" 为用户自定义模板。
|
|
|
|
|
|
"""
|
|
|
|
|
|
name: str
|
|
|
|
|
|
x_ratio: float = 0.0
|
|
|
|
|
|
y_ratio: float = 0.0
|
|
|
|
|
|
width_ratio: float = 0.3
|
|
|
|
|
|
height_ratio: float = 0.3
|
|
|
|
|
|
rotation: float = 0.0
|
|
|
|
|
|
type: str = "custom" # "builtin" | "custom"
|
|
|
|
|
|
|
2026-06-16 15:11:56 +08:00
|
|
|
|
def to_transform_state(
|
|
|
|
|
|
self,
|
|
|
|
|
|
garment_width: float,
|
|
|
|
|
|
garment_height: float,
|
|
|
|
|
|
print_width: float = None,
|
|
|
|
|
|
print_height: float = None,
|
|
|
|
|
|
) -> TransformState:
|
|
|
|
|
|
"""将比例参数转换为针对指定衣服尺寸的像素坐标 TransformState。
|
|
|
|
|
|
|
|
|
|
|
|
width_ratio / height_ratio 定义衣服上的目标框。若提供印花原始尺寸
|
|
|
|
|
|
(print_width / print_height),印花按原始宽高比缩放后 contain 进目标框
|
|
|
|
|
|
并居中,避免被拉伸变形;未提供时退化为直接铺满目标框(旧行为)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
box_x = self.x_ratio * garment_width
|
|
|
|
|
|
box_y = self.y_ratio * garment_height
|
|
|
|
|
|
box_w = self.width_ratio * garment_width
|
|
|
|
|
|
box_h = self.height_ratio * garment_height
|
|
|
|
|
|
|
|
|
|
|
|
if print_width and print_height and print_width > 0 and print_height > 0:
|
|
|
|
|
|
scale = min(box_w / print_width, box_h / print_height)
|
|
|
|
|
|
draw_w = print_width * scale
|
|
|
|
|
|
draw_h = print_height * scale
|
|
|
|
|
|
cx = box_x + box_w / 2.0
|
|
|
|
|
|
cy = box_y + box_h / 2.0
|
|
|
|
|
|
return TransformState(
|
|
|
|
|
|
x=cx - draw_w / 2.0,
|
|
|
|
|
|
y=cy - draw_h / 2.0,
|
|
|
|
|
|
width=draw_w,
|
|
|
|
|
|
height=draw_h,
|
|
|
|
|
|
rotation=self.rotation,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-15 16:56:36 +08:00
|
|
|
|
return TransformState(
|
2026-06-16 15:11:56 +08:00
|
|
|
|
x=box_x,
|
|
|
|
|
|
y=box_y,
|
|
|
|
|
|
width=box_w,
|
|
|
|
|
|
height=box_h,
|
2026-06-15 16:56:36 +08:00
|
|
|
|
rotation=self.rotation,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 导出选项
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class ExportOptions:
|
|
|
|
|
|
"""单张或批量导出时的输出配置。"""
|
|
|
|
|
|
output_dir: str = "" # 空字符串 = 使用程序默认 output/ 目录
|
|
|
|
|
|
output_format: str = "PNG" # "PNG" | "JPG"
|
|
|
|
|
|
quality: int = 95 # JPG 质量 1-95,PNG 时忽略
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 批量选项
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class BatchMode(str, Enum):
|
|
|
|
|
|
"""批量合成的配对模式。"""
|
|
|
|
|
|
ONE_TO_ONE = "one_to_one" # 一一匹配
|
|
|
|
|
|
MANY_GARMENTS = "many_garments" # 多衣服 × 单印花
|
|
|
|
|
|
MANY_PRINTS = "many_prints" # 单衣服 × 多印花
|
|
|
|
|
|
FULL_COMBO = "full_combo" # 全组合(矩阵)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-16 11:21:21 +08:00
|
|
|
|
class VisibilityStatus(str, Enum):
|
|
|
|
|
|
"""衣服目标区域与印花的可见度状态。"""
|
|
|
|
|
|
NORMAL = "正常"
|
|
|
|
|
|
LOW = "偏低"
|
|
|
|
|
|
UNCLEAR = "不明显"
|
|
|
|
|
|
UNKNOWN = "无法判断"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 16:56:36 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class BatchOptions:
|
|
|
|
|
|
"""批量任务配置。"""
|
|
|
|
|
|
mode: BatchMode = BatchMode.FULL_COMBO
|
|
|
|
|
|
export_options: ExportOptions = field(default_factory=ExportOptions)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-16 11:21:21 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class VisibilityResult:
|
|
|
|
|
|
"""衣服目标区域与印花颜色对比分析结果。"""
|
|
|
|
|
|
status: VisibilityStatus
|
|
|
|
|
|
score: float = 0.0
|
|
|
|
|
|
rgb_distance: float = 0.0
|
|
|
|
|
|
brightness_difference: float = 0.0
|
|
|
|
|
|
error: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 17:30:18 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# AI 穿搭
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class OutfitTask:
|
|
|
|
|
|
"""AI 穿搭单行任务。
|
|
|
|
|
|
|
|
|
|
|
|
row_index: Excel 行号(从 1 开始,便于写回 D/E/F 列)。
|
|
|
|
|
|
status: 待处理 / 生成中 / 完成 / 失败 / 跳过。
|
|
|
|
|
|
"""
|
|
|
|
|
|
row_index: int
|
|
|
|
|
|
title: str
|
|
|
|
|
|
product_id: str
|
|
|
|
|
|
garment_path: str
|
|
|
|
|
|
status: str = "待处理"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class OutfitResult:
|
2026-06-22 17:03:04 +08:00
|
|
|
|
"""AI 穿搭单行生成结果。
|
|
|
|
|
|
|
|
|
|
|
|
output_path: 单文件行 = 结果图路径;目录行 = 输出子目录路径(写回 Excel D 列)。
|
|
|
|
|
|
output_paths: 目录行下每张结果图的路径(供缩略图逐张展示);单文件行留空。
|
|
|
|
|
|
"""
|
2026-06-18 17:30:18 +08:00
|
|
|
|
task: OutfitTask
|
|
|
|
|
|
success: bool
|
|
|
|
|
|
output_path: str = ""
|
|
|
|
|
|
error: str = ""
|
|
|
|
|
|
attempts: int = 0
|
2026-06-22 17:03:04 +08:00
|
|
|
|
output_paths: List[str] = field(default_factory=list)
|
2026-06-18 17:30:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-23 11:04:38 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class TitleResult:
|
|
|
|
|
|
"""AI 标题生成单行结果(docs/11 §17)。
|
|
|
|
|
|
|
|
|
|
|
|
generated_title: 成功时为 AI 生成、清洗后的单行标题(写回 Excel A 列)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
task: OutfitTask
|
|
|
|
|
|
success: bool
|
|
|
|
|
|
generated_title: str = ""
|
|
|
|
|
|
error: str = ""
|
|
|
|
|
|
attempts: int = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 16:56:36 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 合成结果
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class ComposeResult:
|
|
|
|
|
|
"""单张合成任务的结果。"""
|
|
|
|
|
|
success: bool
|
|
|
|
|
|
garment_path: Optional[Path] = None
|
|
|
|
|
|
print_path: Optional[Path] = None
|
|
|
|
|
|
output_path: Optional[Path] = None # 成功时有效
|
|
|
|
|
|
error: str = "" # 失败时的错误说明
|
2026-06-16 11:21:21 +08:00
|
|
|
|
visibility: Optional[VisibilityResult] = None
|
2026-06-15 16:56:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class BatchResult:
|
|
|
|
|
|
"""批量合成任务的汇总结果。"""
|
|
|
|
|
|
results: List[ComposeResult] = field(default_factory=list)
|
|
|
|
|
|
total: int = 0
|
|
|
|
|
|
success_count: int = 0
|
|
|
|
|
|
failure_count: int = 0
|