fix: 修改一下生图的 bug

This commit is contained in:
2026-08-31 21:56:48 +08:00
parent 2a667b4f3b
commit 8b54f7ed1e
14 changed files with 636 additions and 47 deletions
+154 -15
View File
@@ -27,6 +27,16 @@ ALLOWED_KINDS = [
# 「生图要求」明确写了「N 张」时按用户指定的来,上限放宽到 24。
PLAN_SAFETY_MAX = 15
PLAN_REQUIREMENTS_MAX = 24
MAX_VARIANT_MAIN_IMAGES = 2
# 规划器的图类归并。不同模型会把“细节图”写成材质图、卖点图或详情图,
# 因此均视作同一类覆盖,而不是强迫模型只使用 material。
SCENE_KINDS = {"lifestyle", "multi_scene"}
DETAIL_KINDS = {"material", "key_features", "selling_pt", "ecommerce_detail"}
_DIMENSION_PATTERN = re.compile(
r"尺寸|长宽高|直径|厚度|(?:长|宽|高)\s*[::×x*]?\s*\d+|\d+(?:\.\d+)?\s*(?:厘米|毫米|cm|mm|inch|in)\b",
re.IGNORECASE,
)
def _total_budget(requirements: str | None) -> int:
@@ -47,12 +57,12 @@ SYSTEM_PROMPT = """你是一名资深电商视觉策划。根据商品信息规
5. variant_name 只能从「SKU规格」列表原样照抄;没有绑定就输出 null。
## 规划规则
1. 先保证 SKU 主图(规则 2),其余类型(场景/细节/尺寸/合集等)按商品特点自行取舍并分配张数,追求信息效率,不为凑数出图。
2. SKU 主图:每个带图 SKU 出 1 张独立主图(kind=white_bg),variant_name 填对应规格名;单 SKU 出 1 张(variant_name=null)。
3. 场景图(kind=lifestyle):按核心使用场景出 1-3 张,每张聚焦一个场景。
4. 细节图(kind=material 或 custom):按关键细节/材质/结构出 1-2 张,每张聚焦一个卖点。
5. 尺寸标注图(kind=size_chart):参数含长宽高/尺寸时出 1 张。
6. SKU 合集图(kind=sku_collection):SKU >1 时出 1 张。
1. 套图必须均衡覆盖:白底主图、场景图、细节图;参数含可靠的长宽高/尺寸时还必须有尺寸标注图。按这个顺序先预留张数,再安排其他类型。
2. 白底主图(kind=white_bg)通常只出 1 张;有多个视觉差异明显的 SKU 时最多选 2 个代表款各出 1 张,variant_name 填对应规格名。**禁止因为 SKU 多而为每个 SKU 都规划主图**。仅当「生图要求」明确要求每个/全部 SKU 分别出图时才可例外。
3. 场景图(kind=lifestyle):至少 1 张,按核心使用场景出 1-2 张,每张聚焦一个场景。
4. 细节图(kind=material / key_features / selling_pt / ecommerce_detail):至少 1 张,展示关键细节、材质或结构;通常 1-2 张,每张聚焦一个卖点。
5. 尺寸标注图(kind=size_chart):参数含长宽高/尺寸时必须出 1 张;无可靠尺寸参数时不要虚构尺寸。
6. SKU 合集图(kind=sku_collection):仅在 SKU >1 且确有必要展示差异时出 1 张,优先级低于上述四类。
7. kind 枚举:white_bg / key_features / selling_pt / material / lifestyle / multi_scene / ecommerce_detail / size_chart / sku_collection / custom。
8. title 用中文短语(如「主图·粉色」「浴室壁挂」);detail 中文说明这张图展示什么;prompt_hint 用英文描述构图要点。
@@ -114,6 +124,141 @@ def _normalize_items(raw_items: list, sku_variants: list[str]) -> list[dict]:
return items
def _has_dimensions(product_info: dict) -> bool:
"""仅在已采集到尺寸信息时规划尺寸图,避免模型编造数值。"""
for param in product_info.get("params") or []:
if not isinstance(param, dict):
continue
text = f"{param.get('key') or ''} {param.get('value') or ''}"
if _DIMENSION_PATTERN.search(text):
return True
return False
def _requires_every_sku(requirements: str | None) -> bool:
"""用户显式要求逐 SKU 出图时,保留其优先级,不施加代表款上限。"""
if not requirements:
return False
return bool(re.search(
r"(?:每个|全部|所有|逐个|分别).{0,10}(?:sku|SKU|规格|款式|颜色|变体)",
requirements,
))
def _coverage_group(kind: str) -> str:
if kind == "white_bg":
return "main"
if kind in SCENE_KINDS:
return "scene"
if kind in DETAIL_KINDS:
return "detail"
if kind == "size_chart":
return "size"
return "other"
def _total(items: list[dict]) -> int:
return sum(item["count"] for item in items)
def _group_total(items: list[dict], group: str) -> int:
return sum(item["count"] for item in items if _coverage_group(item["kind"]) == group)
def _remove_one(items: list[dict], predicate) -> bool:
"""从后往前删除一个生成槽位,优先保留模型较早规划的构图。"""
for index in range(len(items) - 1, -1, -1):
if not predicate(items[index]):
continue
if items[index]["count"] > 1:
items[index]["count"] -= 1
else:
items.pop(index)
return True
return False
def _make_room(items: list[dict], budget: int, minimums: dict[str, int]) -> bool:
"""为必需图类腾出一张,先去除非核心图或某类的冗余张数。"""
candidates: list[tuple[int, int]] = []
for index, item in enumerate(items):
group = _coverage_group(item["kind"])
if _group_total(items, group) <= minimums.get(group, 0):
continue
# other 最低优先;同类冗余时从后面的项开始删。
priority = 0 if group == "other" else 1
candidates.append((priority, -index))
if not candidates:
return False
_, neg_index = min(candidates)
index = -neg_index
item = items[index]
if item["count"] > 1:
item["count"] -= 1
else:
items.pop(index)
return True
def _balanced_items(
items: list[dict],
sku_variants: list[str],
product_info: dict,
requirements: str | None,
budget: int,
) -> list[dict]:
"""将 LLM 方案约束为信息均衡的商品套图。
提示词能改善规划倾向,但不能保证每次输出。这里把 SKU 主图总量限制为
代表款数量,并在预算允许时补齐核心图类,避免 SKU 多时挤掉场景/细节/尺寸图。
"""
balanced = [dict(item) for item in items if item["count"] > 0]
if not _requires_every_sku(requirements):
max_mains = 1 if len(sku_variants) <= 1 else MAX_VARIANT_MAIN_IMAGES
while sum(item["count"] for item in balanced if item["kind"] == "white_bg") > max_mains:
_remove_one(balanced, lambda item: item["kind"] == "white_bg")
required: list[tuple[str, dict]] = [
("main", {
"kind": "white_bg", "title": "白底主图", "detail": "商品白底主视觉",
"prompt_hint": "front view on pure white background", "count": 1,
"variant_name": sku_variants[0] if sku_variants else None,
}),
("scene", {
"kind": "lifestyle", "title": "使用场景", "detail": "展示商品实际使用场景",
"prompt_hint": "product in a natural everyday use setting", "count": 1,
"variant_name": None,
}),
("detail", {
"kind": "material", "title": "细节展示", "detail": "突出材质与结构细节",
"prompt_hint": "close-up of authentic material and construction details", "count": 1,
"variant_name": None,
}),
]
if _has_dimensions(product_info):
required.append(("size", {
"kind": "size_chart", "title": "尺寸标注", "detail": "展示已采集的商品尺寸",
"prompt_hint": "clear size chart using known product dimensions", "count": 1,
"variant_name": None,
}))
# 用户明确要求较小张数时,优先保留前面的核心图类;不突破其指定预算。
required = required[:budget]
minimums = {group: 1 for group, _ in required}
for group, fallback in required:
if _group_total(balanced, group) > 0:
continue
while _total(balanced) >= budget and _make_room(balanced, budget, minimums):
pass
if _total(balanced) < budget:
balanced.append(fallback)
while _total(balanced) > budget and _make_room(balanced, budget, minimums):
pass
return balanced
def _repair_truncated(s: str) -> dict | None:
"""截断修复:从最后一个完整的 '}' 处截断,剥尾逗号后按括号配平补全闭合。
@@ -234,16 +379,10 @@ async def generate_plan(
items = _normalize_items(data.get("items") or [], sku_variants)
if items:
# 总量保护:只防跑飞不干预正常规划。生图要求指定「N 张」时按 N 放宽上限
# 先压缩多 SKU 的重复主图、补齐核心视觉类别,再执行总量保护。
# 生图要求指定「N 张」时按 N 放宽上限,并始终不突破该预算。
budget = _total_budget(requirements)
total = sum(i["count"] for i in items)
while total > budget and items:
last = items[-1]
if last["count"] > 1:
last["count"] -= 1
else:
items.pop()
total = sum(i["count"] for i in items)
items = _balanced_items(items, sku_variants, product_info, requirements, budget)
return {"summary": str(data.get("summary") or "").strip()[:100], "items": items}
last_error = RuntimeError("规划器未返回有效方案项")