修复种草图生成:以三合一主图为参考 img2img 生成,按颜色分配(每色优先、超出随机补足);新增 BR/CA/DE/ES/IT/PL/SA 七国配置与提示词;删除验证用测试脚本

This commit is contained in:
2026-08-24 14:02:19 +08:00
parent f493bde8a9
commit 3c341d2e78
51 changed files with 2571 additions and 514 deletions
+113 -39
View File
@@ -93,7 +93,13 @@ class TemplateRouter:
raise ValueError(f"工作簿中不存在工作表 {sheet!r},现有: {self.wb.sheetnames}")
self.ws = self.wb[sheet]
# 完整表头 -> 列号 (如 "基础信息-商品层级" -> 1
# 布局行号(自动探测,失败回退模块级常量,兼容旧版模版
self.group_row = GROUP_ROW
self.header_row = HEADER_ROW
self.data_start = DATA_START
self._detect_layout()
# 完整表头 -> 列号 (如 "基础信息-商品层级" -> 1,含括号全名别名)
self.header_map: dict[str, int] = {}
# 列名(去分组) -> 列号 (如 "商品层级" -> 1
self.column_map: dict[str, int] = {}
@@ -101,21 +107,82 @@ class TemplateRouter:
self.col_headers: dict[int, str] = {}
self._build_headers()
# 关键列按列名解析(不依赖固定列号,列名缺失时回退模块级常量)
self.level_col = self.column_map.get("商品层级", LEVEL_COL)
self.spu_code_col = self.column_map.get("SPU货号", SPU_CODE_COL)
self._used_rows: set[int] | None = None # 已占用行缓存,见 _refresh()
# ------------------------------------------------------------------
# 布局自动探测
# ------------------------------------------------------------------
def _detect_layout(self) -> None:
"""按固定分组名定位布局(不依赖固定行列)。
分组名(如「基础信息(SKU/SPU均必填)」「SPU商品属性」)是模版固定不变的,
以其为锚点:
1. 分组行 = 含「基础信息」的行(去括号说明后精确匹配,兼容含括号全名);
2. 列名行 = 分组行正下方的列名行(校验含「商品层级」+「SPU货号」);
3. 数据起始行 = 列名行之后第一个整行空行(跳过说明行)。
探测失败时保持模块级常量默认值。
"""
max_col = min(self.ws.max_column, 30)
def row_texts(row: int) -> list[str]:
return [str(self.ws.cell(row, c).value or "").strip()
for c in range(1, max_col + 1)]
# 1) 分组行:按固定分组名「基础信息」匹配
group_row = None
for row in range(1, min(self.ws.max_row, 30) + 1):
for v in row_texts(row):
if _GROUP_BRACKET.sub("", v).strip() == "基础信息" or "基础信息" in v:
group_row = row
break
if group_row:
break
# 2) 列名行:固定位于分组行正下方(校验含「商品层级」+「SPU货号」;
# 列名可能变化,校验失败时直接用分组行正下方一行)
header_row = None
if group_row:
for row in range(group_row + 1, min(self.ws.max_row, group_row + 4) + 1):
vals = row_texts(row)
if any("商品层级" in v for v in vals) and any("SPU货号" in v for v in vals):
header_row = row
break
if header_row is None:
header_row = group_row + 1
# 3) 数据起始行:列名行正下方第2行(列名行→说明行→数据起始行)。
# 不用「第一个空行」——已填写的模版首个空行在数据之后,会漏扫已有数据。
if header_row:
data_start = header_row + 2
if group_row:
self.group_row = group_row
if header_row:
self.header_row = header_row
if data_start:
self.data_start = data_start
# ------------------------------------------------------------------
# 表头解析
# ------------------------------------------------------------------
def _build_headers(self) -> None:
for col in range(1, self.ws.max_column + 1):
raw_group = self.ws.cell(GROUP_ROW, col).value
raw_name = self.ws.cell(HEADER_ROW, col).value
group = _GROUP_BRACKET.sub("", str(raw_group)).strip() if raw_group else ""
raw_group = self.ws.cell(self.group_row, col).value
raw_name = self.ws.cell(self.header_row, col).value
group_full = str(raw_group).strip() if raw_group else ""
group = _GROUP_BRACKET.sub("", group_full).strip()
name = str(raw_name).strip() if raw_name else ""
if not name:
continue
header = f"{group}-{name}" if group else name
self.header_map[header] = col
if group_full and group_full != group:
# 完整分组名(含括号说明)别名,如「基础信息(SKU/SPU均必填)-商品层级」
self.header_map[f"{group_full}-{name}"] = col
self.column_map[name] = col
self.col_headers[col] = header
@@ -134,19 +201,19 @@ class TemplateRouter:
return m
def _refresh_used_rows(self) -> None:
"""扫描数据区,收集 A 列(商品层级)非空的行号集合。"""
"""扫描数据区,收集商品层级列(按列名解析)非空的行号集合。"""
used: set[int] = set()
for row in range(DATA_START, self.ws.max_row + 1):
v = self.ws.cell(row, LEVEL_COL).value
for row in range(self.data_start, self.ws.max_row + 1):
v = self.ws.cell(row, self.level_col).value
if v is not None and str(v).strip():
used.add(row)
self._used_rows = used
def first_empty_row(self, start: int = DATA_START) -> int:
def first_empty_row(self, start: int | None = None) -> int:
"""返回数据区第一个空行(A 列为空)。"""
if self._used_rows is None:
self._refresh_used_rows()
row = max(start, DATA_START)
row = max(start or self.data_start, self.data_start)
while row in self._used_rows:
row += 1
return row
@@ -161,8 +228,8 @@ class TemplateRouter:
spu_code = str(spu_code).strip().lower()
match = self._normalize_match(match)
hits = []
for row in range(DATA_START, self.ws.max_row + 1):
v = self.ws.cell(row, SPU_CODE_COL).value
for row in range(self.data_start, self.ws.max_row + 1):
v = self.ws.cell(row, self.spu_code_col).value
if v is None:
continue
v = str(v).strip().lower()
@@ -171,15 +238,15 @@ class TemplateRouter:
return hits
def find_sku_rows(self, spu_code: str, match: str = "exact") -> list[int]:
"""按 SPU货号(B列+ 层级=skuA列)查找所有 SKU 行号(单 SPU 多色时无 SPU 行,SKU 行即全部)。"""
"""按 SPU货号(按列名解析+ 层级=sku商品层级列)查找所有 SKU 行号(单 SPU 多色时无 SPU 行,SKU 行即全部)。"""
spu_code = str(spu_code).strip().lower()
match = self._normalize_match(match)
hits = []
for row in range(DATA_START, self.ws.max_row + 1):
lv = self.ws.cell(row, LEVEL_COL).value
for row in range(self.data_start, self.ws.max_row + 1):
lv = self.ws.cell(row, self.level_col).value
if lv is None or str(lv).strip().lower() != "sku":
continue
v = self.ws.cell(row, SPU_CODE_COL).value
v = self.ws.cell(row, self.spu_code_col).value
if v is None:
continue
v = str(v).strip().lower()
@@ -189,7 +256,7 @@ class TemplateRouter:
def find_level_rows(self, level: str, match: str = "exact") -> list[int]:
"""
按 商品层级(A列)查找所有行号。
按 商品层级(按列名解析)查找所有行号。
match="exact"(默认):精准匹配,去首尾空格后完全相等(不区分大小写);
match="fuzzy" :模糊匹配,层级包含关键字即命中(不区分大小写)。
@@ -197,8 +264,8 @@ class TemplateRouter:
level = str(level).strip().lower()
match = self._normalize_match(match)
hits = []
for row in range(DATA_START, self.ws.max_row + 1):
v = self.ws.cell(row, LEVEL_COL).value
for row in range(self.data_start, self.ws.max_row + 1):
v = self.ws.cell(row, self.level_col).value
if v is None:
continue
v = str(v).strip().lower()
@@ -278,13 +345,27 @@ class TemplateRouter:
return column_index_from_string(key_s.upper())
raise KeyError(f"无法识别的表头: {key!r}(可用完整表头/列名/列号/列字母)")
def _value_by_col(self, data: dict, name: str):
"""从 data 中按列名取第一个匹配的值(键可为完整表头/列名/列号/列字母)。"""
col = self.column_map.get(name)
if col is None:
return None
for k, v in data.items():
try:
if self.resolve_col(k) == col:
return v
except KeyError:
continue
return None
def insert(self, data: dict, spu_code: str | None = None,
level: str | None = None, update: bool = False,
match: str = "exact") -> int:
"""
插入一行数据到模版表。data 键支持:完整表头(推荐)、列名、列号、列字母。
- 若 data 中带「商品层级/基础信息-商品层级」则自动取其值路由;
- 若 data 中带「商品层级」(任意键形式,如「基础信息(SKU/SPU均必填)-商品层级」/
「基础信息-商品层级」/「商品层级」)则自动取其值路由;
- 传 spu_code 会把该行路由到对应 SPU 行之后(SKU 关联);
- update=True 时覆盖已存在的目标行,否则写入新空行;
- match 控制货号/层级的匹配模式:exact(精准,默认) / fuzzy(模糊),见 route()。
@@ -293,27 +374,20 @@ class TemplateRouter:
"""
match = self._normalize_match(match)
# 1) 解析层级与 spu_code
lv = level
if lv is None:
for k in ("基础信息-商品层级", "商品层级", LEVEL_COL):
if k in data:
lv = data[k]
break
# 1) 解析层级与 spu_code(按列名匹配,不依赖固定键名/列号)
lv = level if level is not None else self._value_by_col(data, "商品层级")
lv_s = str(lv).strip().lower() if lv is not None else ""
# 只有「SKU 层」才需要按 SPU货号 关联路由;SPU/单sku商品 层走新行路由
use_spu_route = spu_code is not None
if spu_code is None:
if lv is not None and str(lv).strip().lower() == "sku" and "SPU货号" in data:
use_spu_route = True # sku 层 -> 插到所属 SPU 行之后
elif lv is None and "SPU货号" in data and self.find_spu_rows(data["SPU货号"], match):
use_spu_route = True # 无层级但货号已存在 -> 视为该 SPU 的变体行
spu = spu_code if use_spu_route else None
if use_spu_route and spu is None:
spu = data["SPU货号"]
if not self.find_spu_rows(spu, match):
use_spu_route = False # 无 SPU 行(单 SPU 多色,不填 SPU 行)→ 按层级末尾追加
spu = None
# 只有「SKU 层」或未声明层级时才按 SPU货号 关联路由;SPU/单sku商品 层走新行路由
spu = spu_code
use_spu_route = spu is not None
if spu is None and (lv_s == "sku" or lv is None):
spu = self._value_by_col(data, "SPU货号")
if spu is not None:
use_spu_route = True
if use_spu_route and spu is not None and not self.find_spu_rows(spu, match):
use_spu_route = False # 无 SPU 行(单 SPU 多色,不填 SPU 行)→ 按层级末尾追加
spu = None
# 2) 路由到目标行
row = self.route(lv, spu, update=update, match=match)