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:
+86
-32
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user