v92-v105 多国适配 + 模板导出增强 + 生图可靠性优化
- 新增韩国(KR)适配:countries/pinterest 种子词、K-pop 提示词、UI 国家列表 - 爬虫跨运行持久化已采集 URL(.collected_urls.json),同关键词重复搜索采新图 - 模板导出增强:SPU 商品属性列名前缀剥离匹配、尺码下拉框 INDIRECT 动态引用、 SPU 字段映射(袖长/门襟/胸垫等)、季节/印花图案女装映射、建议售价统一必填、 SKU 分类/数量/单位直接填默认值 - 自定义模式:多模态提示词独立(custom_analyze_*)、生图提示词模板可配置 - Pinterest:空选品守卫、连续空分析保护(max_empty_analyze)、flat_prompt 配置覆盖 - 生图可靠性:开始/完成进度日志、异步任务轮询超时 60s→300s
This commit is contained in:
@@ -74,7 +74,6 @@ def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str,
|
||||
"suitable_for_print": True,
|
||||
"design_category": classify(term),
|
||||
"concept": f"围绕「{term}」的原创印花设计",
|
||||
"negative_prompt": str(b.get("negative_prompt") or "").strip(),
|
||||
"image_prompt": str(b.get("image_prompt") or "").strip(),
|
||||
"ref_images": [str(p) for p in (b.get("ref_images") or []) if str(p)],
|
||||
"source_md5": str(b.get("source_md5") or "").strip().lower(),
|
||||
@@ -114,11 +113,11 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if batch_size <= 0:
|
||||
batch_size = max_designs # 自动:一次最多分析 max_designs 张(每张图→1条简报)
|
||||
need = batch_size
|
||||
# 自定义模式:本地图池是唯一且有限的图源,最多分析「选品清单总数」张即可,
|
||||
# 避免多余分析(超出的图源在简报达标后由自定义路由结束,不浪费配额)。
|
||||
if bool(state.get("custom_mode")):
|
||||
_t = int(state.get("pinterest_target") or 1) or 1
|
||||
need = max(0, min(need, _t))
|
||||
# 按目标简报数收敛:最多分析到满足选品清单所需的简报数即可(target=spu_tasks,每款一个设计),
|
||||
# 普通与自定义模式一致——避免单一产品任务也把整池未消费图片全部分析成冗余简报、浪费配额;
|
||||
# 分析出的简报若不适合可丢弃,缺口不足时再由路由补分析/search。
|
||||
_t = int(state.get("pinterest_target") or 1) or 1
|
||||
need = max(1, min(need, _t))
|
||||
|
||||
# 1) 图池取未消费图片(md5 不在 used_images);无 → 返回空,路由触发搜索
|
||||
pool = load_image_pool(output_dir, country)
|
||||
@@ -204,7 +203,8 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if pipe is not None and hasattr(pipe, "record_400"):
|
||||
if pipe.record_400():
|
||||
pipe._abort_current_term()
|
||||
res = llm.analyze_pinterest_images(paths, term, country, on_400=_on_400) or []
|
||||
res = llm.analyze_pinterest_images(paths, term, country, on_400=_on_400,
|
||||
custom_mode=bool(state.get("custom_mode"))) or []
|
||||
# 把每条简报的来源图路径回填为原始图(压缩图仅用于分析,参考图用原图)
|
||||
return _assign_refs(res, chunk)
|
||||
except Exception as e: # noqa: BLE001
|
||||
@@ -269,6 +269,16 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
accumulated = list(state.get("briefs") or [])
|
||||
accumulated.extend(new_briefs)
|
||||
|
||||
# 连续无产出计数器:本次无新增简报 → 递增(防"逐一取下一张参考图"空转);
|
||||
# 有新增简报 → 重置为 0
|
||||
empty_rounds = int(state.get("pinterest_empty_rounds") or 0)
|
||||
if new_briefs:
|
||||
empty_rounds = 0
|
||||
else:
|
||||
empty_rounds += 1
|
||||
print(f"[pinterest_analyze] 本轮无新增简报(连续 {empty_rounds} 轮无产出),"
|
||||
f"超过上限后将自动结束")
|
||||
|
||||
# 推送实际新增且保留的简报到并发生成流水线(简报池):边分析边生成设计/三合一/种草图
|
||||
pushed = accumulated[old_count:]
|
||||
pipe = state.get("pinterest_pipeline")
|
||||
@@ -293,4 +303,5 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
}
|
||||
print(f"[pinterest_analyze] 本轮分析 {len(batch)} 张图 → 简报 {len(new_briefs)} 条,"
|
||||
f"累计 {len(accumulated)} 条({country})")
|
||||
return {"pinterest_briefs": kept, "briefs": accumulated, "stats": stats, "errors": errors}
|
||||
return {"pinterest_briefs": kept, "briefs": accumulated, "stats": stats, "errors": errors,
|
||||
"pinterest_empty_rounds": empty_rounds}
|
||||
|
||||
+50
-19
@@ -22,11 +22,7 @@ PINTEREST_PRINT_SUFFIX = (
|
||||
"no garment, no shirt, no model, no mannequin, no watermark"
|
||||
)
|
||||
|
||||
# Pinterest 生图提示词 4 段结构中第 3 段的引导前缀:把 LLM 产出的 negative_prompt
|
||||
# 转成一条正向「Strictly avoid: ...」条款拼进 image_prompt,让防复制/防商标约束落到生成指令
|
||||
NEG_LEAD = "Strictly avoid: "
|
||||
|
||||
# Pinterest 生图提示词 4 段结构中第 4 段(仅当设计含文字时追加):
|
||||
# Pinterest 生图提示词 3 段结构中第 2 段(仅当设计含文字时追加):
|
||||
# 要求模型把引号内的文字按原文逐字正确拼写,避免乱码/拼错
|
||||
SPELLING_RULE = (
|
||||
"Render every phrase shown in quotes exactly as written, "
|
||||
@@ -43,6 +39,26 @@ REVIEW_REBRAND_HINT = (
|
||||
"a generic, non-infringing homage in the same mood, clearly distinct from the original."
|
||||
)
|
||||
|
||||
# —— 自定义模式(custom)生图模板:固定前缀 + 分析模型 image_prompt + 固定负向 ——
|
||||
# 自定义模式分析模型产出的是「新设计描述」,生图时套用这套固定模板(含防复制/防服装约束),
|
||||
# 负向用固定文本(写入 composite_negative,compose 生图时作为负向参数传给图像后端)。
|
||||
CUSTOM_IMAGE_PROMPT_TEMPLATE = (
|
||||
"Use the attached bestseller product photo only as loose inspiration for "
|
||||
"overall mood, theme, era and style genre — do NOT reproduce, trace, "
|
||||
"rearrange, recolor or closely imitate its printed artwork, characters, "
|
||||
"layout or text, and do NOT render a shirt, garment, model, hanger, photo "
|
||||
"scene, product mockup or background of any kind. First, carefully examine "
|
||||
"the printed artwork on the garment in the attached photo: zoom in mentally "
|
||||
"on the print area, identify its subject, technique, palette and layout, and "
|
||||
"base the new design on THOSE observed traits. Ignore the model, background "
|
||||
"and photo quality. Then: {image_prompt}"
|
||||
)
|
||||
CUSTOM_NEGATIVE_PROMPT = (
|
||||
"copy of reference artwork, lookalike of the bestseller print, characters, "
|
||||
"mascots, likenesses, logos, trademarks, watermark, photorealistic shirt, "
|
||||
"apparel, product mockup, model, garment, hanger, busy background"
|
||||
)
|
||||
|
||||
# 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif,
|
||||
# 避免 gpt-image 等内容策略频繁拦截导致"生图限制多")
|
||||
_IMG_RISKY_SWAP = {
|
||||
@@ -76,6 +92,17 @@ def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
cc = state["country_config"]
|
||||
extra_rules = cc.get("extra_style_rules") or []
|
||||
tpls = config.get("prompt_templates") or {}
|
||||
# Pinterest 生图提示词固定段:可配置(config.pinterest.prompt_pieces),留空/缺失回退内置常量。
|
||||
# 自定义模式(custom_mode)用独立的一套固定段(config.custom.prompt_pieces),默认与 Pinterest 相同、可单独编辑。
|
||||
if bool(state.get("custom_mode")):
|
||||
pp = (config.get("custom") or {}).get("prompt_pieces") or {}
|
||||
else:
|
||||
pp = (config.get("pinterest") or {}).get("prompt_pieces") or {}
|
||||
print_suffix = (pp.get("print_suffix") or "").strip() or PINTEREST_PRINT_SUFFIX
|
||||
spelling_rule = (pp.get("spelling_rule") or "").strip() or SPELLING_RULE
|
||||
review_rebrand_hint = (pp.get("review_rebrand_hint") or "").strip() or REVIEW_REBRAND_HINT
|
||||
custom_ip_tpl = (pp.get("image_prompt_template") or "").strip() or CUSTOM_IMAGE_PROMPT_TEMPLATE
|
||||
custom_neg = (pp.get("negative_prompt") or "").strip() or CUSTOM_NEGATIVE_PROMPT
|
||||
|
||||
briefs: List[Dict[str, Any]] = []
|
||||
for r in screened:
|
||||
@@ -95,20 +122,24 @@ def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country)
|
||||
llm_ip = (r.get("image_prompt") or "").strip()
|
||||
llm_neg = (r.get("negative_prompt") or "").strip()
|
||||
if r.get("source") == "pinterest" and llm_ip:
|
||||
# —— Pinterest 参考模式:跳过四要素模板,按 4 段结构拼 image_prompt ——
|
||||
# ① image_prompt(分析模型产出) + 固定输出形态后缀 PINTEREST_PRINT_SUFFIX
|
||||
# ② 负向条款(由 LLM negative_prompt 经 NEG_LEAD 引导,转化进正向指令)
|
||||
# ③ 拼写锁定句 SPELLING_RULE(仅当 LLM image_prompt 已含引号文字段时)
|
||||
# 是否含文字、拼写与否均由分析模型产出决定,本模式不注入 slogan。
|
||||
seg: List[str] = [llm_ip, PINTEREST_PRINT_SUFFIX.strip()]
|
||||
if llm_neg:
|
||||
seg.append(NEG_LEAD + llm_neg)
|
||||
if '"' in llm_ip:
|
||||
seg.append(SPELLING_RULE)
|
||||
prompts["image_prompt"] = ", ".join(seg)
|
||||
print(f"[prompt] Pinterest 简报按 4 段结构拼 image_prompt(跳过四要素模板): 「{r['topic']}」")
|
||||
if bool(state.get("custom_mode")):
|
||||
# —— 自定义模式:固定模板(前缀 + 分析 image_prompt),负向用固定文本 ——
|
||||
# 模板含防复制/防服装约束,不再追加 print_suffix;负向写入 composite_negative,
|
||||
# compose 生图时作为负向参数传给图像后端。
|
||||
prompts["image_prompt"] = custom_ip_tpl.replace("{image_prompt}", llm_ip)
|
||||
prompts["composite_negative"] = custom_neg
|
||||
print(f"[prompt] 自定义模式按固定模板拼 image_prompt(含固定负向): 「{r['topic']}」")
|
||||
else:
|
||||
# —— Pinterest 参考模式:跳过四要素模板,按 3 段结构拼 image_prompt ——
|
||||
# ① image_prompt(分析模型产出) + 固定输出形态后缀 PINTEREST_PRINT_SUFFIX
|
||||
# ② 拼写锁定句 SPELLING_RULE(仅当 LLM image_prompt 已含引号文字段时)
|
||||
# 是否含文字、拼写与否均由分析模型产出决定,本模式不注入 slogan。
|
||||
seg: List[str] = [llm_ip, print_suffix]
|
||||
if '"' in llm_ip:
|
||||
seg.append(spelling_rule)
|
||||
prompts["image_prompt"] = ", ".join(seg)
|
||||
print(f"[prompt] Pinterest 简报按 3 段结构拼 image_prompt(跳过四要素模板): 「{r['topic']}」")
|
||||
else:
|
||||
# —— 热点采集模式:四要素模板装配 + 文字印花(约 30% 概率注入 slogan)——
|
||||
slogan = (r.get("slogan") or "").strip()
|
||||
@@ -119,7 +150,7 @@ def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
r["used_slogan"] = slogan
|
||||
# review(疑似商标/受保护主题)→ 追加「原创化魔改」引导(两个模式通用)
|
||||
if str(r.get("risk_level", "")).strip().lower() == "review":
|
||||
prompts["image_prompt"] = prompts["image_prompt"] + " " + REVIEW_REBRAND_HINT
|
||||
prompts["image_prompt"] = prompts["image_prompt"] + " " + review_rebrand_hint
|
||||
print(f"[prompt] review 简报注入原创化魔改引导: 「{r['topic']}」")
|
||||
r.update(prompts)
|
||||
r["motif"] = motif
|
||||
|
||||
@@ -67,7 +67,8 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
db_path = root / db_path
|
||||
break
|
||||
|
||||
from graph.template_export import export_products
|
||||
from graph.template_export import (export_products, _resolve_component_map,
|
||||
_resolve_season_map, _resolve_pattern_map)
|
||||
tdir = (pcfg.get("template_dir") or "").strip() or str(Path(tp).parent)
|
||||
prod_dir = output_dir / "product"
|
||||
prod_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -120,6 +121,9 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
db_path, batch, tdir, tp, str(out),
|
||||
markup_percent=float(pcfg.get("markup_percent") or 0),
|
||||
suggested_price_ratio=float(pcfg.get("suggested_price_ratio") or 0),
|
||||
component_map=_resolve_component_map(config),
|
||||
season_map=_resolve_season_map(config),
|
||||
pattern_map=_resolve_pattern_map(config),
|
||||
)
|
||||
for r in products:
|
||||
if (r.get("composite_path") or r.get("printed_path")) and (r.get("en_title") or "").strip():
|
||||
|
||||
Reference in New Issue
Block a user