v89-v91 模板增强 + 图源映射 + 多模态提示词可配置化

- 图源映射统一:热点采集与 Pinterest 模式均走 config.product.mark_dirs 配置,按任务序号随机抽模特图/平铺图
- 商品产地固定:统一为「中国大陆」+「产地省份=广东省」(不再读站点/字典映射)
- 模板 SKU 字段检测:按建议售价同一套路检测 SKU分类/SKU数量/SKU数量单位,必填时填入单品/1/件
- 多模态分析提示词可配置:prompts/pinterest_analyze_system.md + user.md,支持国家覆盖,不丢文件回退内置
- 自定义图片模式:新增 pinterest_custom_load_node,图片数量硬校验,选品清单 ≤ 有效图片数
- 模板导出优化:写入前按货号末 3 位升序排序,不再产生空白 xlsx
- 修复 v90 project review 10 项(503 致命终止、线程安全、原子写入等)
This commit is contained in:
2026-08-28 16:24:50 +08:00
parent 2a96ec0870
commit 71a48e4ed5
40 changed files with 780 additions and 200 deletions
+86 -32
View File
@@ -83,6 +83,49 @@ MODEL_WEAR_PROMPT = (
"图1原本的背景、人物、构图及光影结构100%不变,仅替换图1衣服上的印花图案与衣服底色。"
)
# 平铺图专属三图合成提示词(mark 配置 flat_dir 平铺图文件夹):图1平铺实拍 + 图2印花 + 图3底图
FLAT_LAY_PROMPT = (
"你是一个专业的电商AI视觉合成工具,执行“高保真印花与色彩移植/印花替换”:把图2的印花设计"
"印到图3底色的面料上,替换图1平铺衣服原有的底色与图案,输出一张“图3底色+图2印花”的平铺服装商品图。"
"全程无人参与。\n"
"【图片角色,按提交顺序】\n"
"图1=衣服平铺实拍图(基底图:提供衣服版型、轮廓、褶皱、光影、拍摄背景与构图,"
"最终输出必须与图1同角度、同摆放);\n"
"图2=纯印花设计稿;\n"
"图3=平铺衣服底图(只取衣服本身的底色与面料材质,忽略平铺图背景/桌面/场景,只保留面料颜色与质感)。\n"
"TASK: 把图2的印花设计印到图3底色的面料上,替换图1平铺衣服原有的底色与图案,"
"输出一张“图3底色+图2印花”的平铺服装商品图。全程无人参与。\n"
"【执行规则】\n"
"0.禁止人物:输出图中严禁出现任何人体、模特、人台/假人、头颈、手臂或“穿着效果”,"
"必须保持图1的纯平铺俯拍商品图形式,衣服平放在原背景上。\n"
"1.底色锁定:从图3提取衣服底色与面料,最终合成中必须100%保持不变,严禁偏色。\n"
"2.印花提取:从图2精准提取纯印花图案(线条/色号/比例),叠加到图3底色上形成合成面料。\n"
"3.印花尺寸适配:印花整体尺寸与衣服面料面积成合理比例,居中印在胸/背/衣身主体区域,"
"占衣身面积约30%-45%,四周留白,严禁过大撑满整件或过小(低于20%)。\n"
"4.主体遮罩:识别图1中衣服本体的完整区域(领口到下摆、含袖子;忽略背景/桌面/无关物品),"
"用合成面料完整覆盖,彻底清除原衣服的颜色与图案;图1中衣服以外的物品保持原样。\n"
"5.精准贴合:合成面料严格跟随图1衣服的平铺形态,领口/袖口/下摆/侧缝等版型结构清晰保留,"
"褶皱/翻折/堆叠处印花随之自然变形,杜绝“贴纸感”与“平面涂色感”。\n"
"6.光影融合:按图1拍摄光线方向调整亮度/对比度,印花随褶皱产生明暗变化但色号不偏移;"
"衣服投影与图1保持一致。\n"
"7.纯净输出:仅输出一张最终合成平铺图;图1的背景/桌面/构图/角度/光影100%不变,"
"仅替换衣服的底色与印花;画面中不得出现任何人像、肢体或人台。"
)
def _active_prompt(kind: str, prompts: Optional[Dict] = None) -> str:
"""返回当前图源应使用的合成提示词。
kind="model" → 模特三图提示词(配置覆盖优先生效,否则内置 MODEL_WEAR_PROMPT);
kind="flat" → 平铺三图提示词(配置覆盖优先生效,否则内置 FLAT_LAY_PROMPT)。
"""
cfgs = prompts or {}
if kind == "flat":
return (str(cfgs.get("flat_prompt") or "").strip()
or FLAT_LAY_PROMPT)
return (str(cfgs.get("model_prompt") or "").strip()
or MODEL_WEAR_PROMPT)
def _resolve_sku(db_path, basemap_root, spu_code: str, sku_code: str, colors=None) -> Optional[str]:
"""选定 SKU:显式指定优先;否则第一个有本地底图的;再无则第一个颜色(便于模板导出)。"""
@@ -144,13 +187,15 @@ def _process_spu(
db_path, basemap_root, material_root, category, prod_dir, brief, ib,
spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="",
img_code="", model_img=None, design_size="1024x1024", compose_size="1536x2048",
on_503=None,
on_503=None, model_kind="model", prompts=None,
) -> Optional[Dict[str, Any]]:
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
img_code: 货号(前缀+3位计数);本产品所有图片文件归入 prod_dir/{img_code}/ 子文件夹
(按货号命名,包含该货号对应的所有图片)。
on_503: 致命图像服务错误(503/账户不可用)回调(供调用方提前终止任务)。
model_kind: 图源类型 "model"(模特图)/ "flat"(平铺图),决定用哪个合成提示词。
prompts: {"model": str, "flat": str} 可配置提示词覆盖(来自 config.product.mark_dirs);缺省用内置。
返回 result dict;内部异常已兜底,不中断。
"""
# 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里
@@ -253,40 +298,43 @@ def _process_spu(
# 6) 模板选择按 SPU.mark 决定:
# mark==1 → 新三图合成模板 MODEL_WEAR_PROMPT(图1模特 + 图2印花设计 + 图3底图)
# mark!=1 → 旧两图合成模板 composite_prompt(底图 + 印花设计)
# mark==1 统一只做三合一:无模特图时跳过合成,不再回退两图合成(印花+底图)
# mark==1 统一只做三合一:无图(模特/平铺)时跳过合成,不再回退两图合成(印花+底图)
if int(spu.get("mark") or 0) == 1:
print(f"{tag} SPU {spu['code']} mark=1 → 使用三图合成模板(图1模特+图2印花+图3底图)")
kind_label = "平铺" if model_kind == "flat" else "模特"
print(f"{tag} SPU {spu['code']} mark=1,图源={kind_label} → 使用三图合成模板(图1{kind_label}+图2印花+图3底图)")
if model_img is not None:
# 任务级模特分配(product_node 预分配:一个 SPU 一个模特,SPU 数>模特数循环兜底
model_copy = prod_dir / f"{img_code}_model{model_img.suffix}"
# 任务级图源分配(pipeline 预分配:mark=1 在有图的模特/平铺文件夹间随机抽
model_copy = prod_dir / f"{img_code}_{model_kind}{model_img.suffix}"
shutil.copy2(model_img, model_copy)
result["model_path"] = str(model_copy)
result["model_folder"] = model_img.parent.name
print(f"{tag} 模特图(任务级分配,{model_img.parent.name}/: {model_copy}")
result["model_kind"] = model_kind
print(f"{tag} {kind_label}图(任务级分配,{model_img.parent.name}/: {model_copy}")
else:
print(f"{tag} material_library 无模特图,mark=1 统一只做三合一,跳过合成")
print(f"{tag} material_library 无{kind_label}图,mark=1 统一只做三合一,跳过合成")
else:
print(f"{tag} SPU {spu['code']} mark={spu.get('mark')} → 使用两图合成模板 composite_prompt(底图+印花)")
# 7) 合成:
# 有模特图 → 三图合成(图1=模特 / 图2=印花设计 / 图3=底图)
# mark=1 无模特图 → 跳过合成(统一只做三合一,不做印花+底图两图合成)
# mark!=1 无模特图 → 两图合成平铺服装图(图3=底图 + 图2=印花设计)
# mark=1 有图(模特/平铺)→ 三图合成(图1=模特或平铺 / 图2=印花设计 / 图3=底图)
# mark=1 无图 → 跳过合成(统一只做三合一,不做印花+底图两图合成)
# mark!=1 无图 → 两图合成平铺服装图(图3=底图 + 图2=印花设计)
if "design_path" not in result:
print(f"{tag} 无设计稿,跳过合成")
elif model_img is not None:
composite_path = str(prod_dir / f"{img_code}_composite.png")
try:
# 三图合成:优先用简报的 composite_prompt(模板化三图文案),回退内置 MODEL_WEAR_PROMPT
wear_prompt = (brief.get("composite_prompt") or "").strip() or MODEL_WEAR_PROMPT
print(f"{tag} 三图合成提交中(3 参考图 img2img,网关处理约 2-6 分钟,请耐心等待)…")
# 三图合成:按图源类型选提示词(可配置覆盖优先,否则内置 MODEL_WEAR/FLAT_LAY
wear_prompt = _active_prompt(model_kind, prompts)
kind_label = "平铺" if model_kind == "flat" else "模特"
print(f"{tag} {kind_label}三图合成提交中(3 参考图 img2img,网关处理约 2-6 分钟,请耐心等待)…")
t0 = time.time()
ib.print(wear_prompt, str(model_img), composite_path,
brief.get("composite_negative", ""),
extra_images=[design_path, str(basemap_img)], # 图2印花, 图3底图
size=compose_size) # 合成图尺寸按 config compose.size
result["composite_path"] = composite_path
print(f"{tag} 三图模特合成图已生成(耗时 {int(time.time()-t0)}s: {composite_path}")
print(f"{tag} {kind_label}三图合成图已生成(耗时 {int(time.time()-t0)}s: {composite_path}")
except Exception as e: # noqa: BLE001
# 合成失败 → 带退避重试(网关超载/超时常见,重试 3 次)
if _fatal(e):
@@ -359,7 +407,7 @@ def _process_spu(
continue
cp = str(prod_dir / f"{img_code}_{str(sc).split('-')[-1]}_composite.png")
try:
ib.print(MODEL_WEAR_PROMPT, str(model_img), cp,
ib.print(_active_prompt(model_kind, prompts), str(model_img), cp,
brief.get("composite_negative", ""),
extra_images=[design_path, str(bm)], # 图2印花, 图3该色底图
size=compose_size) # 合成图尺寸按 config compose.size
@@ -493,25 +541,28 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
prod_dir = output_dir / "product"
prod_dir.mkdir(parents=True, exist_ok=True)
# 4.1) 任务级模特分配(按 spu.mark 映射模特目录 → 过滤非 3:4 图片 → 每个任务随机抽取):
# 每个产品任务(含同一 SPU 的多个款)都独立随机抽一个模特,保证同款多产品模特不重复
# 4.1) 任务级图源分配(与 Pinterest 模式一致):按 config.product.mark_dirs 可配置映射,
# 在「模特图/平铺图」两个有图的文件夹间随机抽图,每张携带 kind(model/flat
# → product_node 用对应提示词合成(模特三图 / 平铺三图)
model_assign: Dict[int, Any] = {}
mark_dirs = pcfg.get("mark_dirs") or {}
prompts_cfg = (mark_dirs.get("1") or {})
try:
from graph.product import build_mark_model_map, find_model_images_for_mark
mark_map = build_mark_model_map(db_path, material_root)
from graph.product import build_mark_sources
sources = build_mark_sources(material_root, mark_dirs, category)
except Exception: # noqa: BLE001
mark_map = {}
for _i, (_spu, _skus, _tb) in enumerate(worklist):
mark = str(_spu.get("mark") or "").strip() or "1"
folder = mark_map.get(mark, category)
try:
pool_imgs = find_model_images_for_mark(db_path, material_root, mark, folder)
except Exception: # noqa: BLE001
pool_imgs = []
if pool_imgs:
model_assign[_i] = random.choice(pool_imgs) # 过滤后随机抽(按任务序号)
if model_assign:
print(f"[product] 任务级模特分配:{len(model_assign)} 个产品任务(按 mark 过滤 3:4 后随机抽取)")
sources = {"model": [], "flat": []}
source_pool = []
for kind in ("model", "flat"):
for p in sources.get(kind) or []:
source_pool.append((p, kind))
if source_pool:
for _i in range(len(worklist)):
img, kind = random.choice(source_pool) # 每任务随机抽一张(含 kind
model_assign[_i] = {"img": img, "kind": kind, "prompts": prompts_cfg}
print(f"[product] 任务级图源分配:{len(model_assign)} 个产品任务(mark_dirs 模特/平铺图随机抽,含 kind)")
else:
print("[product] material_library 无任何可用图(模特/平铺均无)→ 跳过合成,仅导出模板")
# 5) compose 节点生成的共享设计稿(图2):每个任务用自己的热点简报设计(tb.design_path),
# 一个货号对应一个设计(多颜色共用该设计),不再所有产品共用第一个
@@ -571,10 +622,13 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
design_path = design_src
tb = dict(tb)
tb["design_path"] = design_path
_src = model_assign.get(wi) or {}
r = _process_spu(db_path, basemap_root, material_root, category, prod_dir,
tb, ib, spu, skus, pcfg, _safe_errors, design_path, title_backend,
country, img_code=img_code,
model_img=model_assign.get(wi), # 按任务序号取独立随机模特
model_img=_src.get("img"), # 按任务序号取独立随机图源
model_kind=_src.get("kind", "model"),
prompts=_src.get("prompts"),
design_size=str((config.get("compose") or {}).get("design_size") or "1024x1024"),
compose_size=str((config.get("compose") or {}).get("size") or "1536x2048"))
if r: