Files
pod_trend_agent/graph/agent.py
T
3218485270 b7f429db89 模板导出增强 + 模特性别分组 + 三合一提示词精简
1) 模板导出:识别「基码表-胸围」填 sku.bust(多个胸围列都填);申报价格模糊匹配多列统一按加价后价格填写;详情图文不再拼接 img_url_2;SPU 款式来源统一填「现货款」;商品产地国家简称映射(沙特→沙特阿拉伯)
2) 模特性别分组:model_features 按男女分组,按模板类目含男/女固定取对应性别模特(含 Pinterest 模式 pipeline)
3) 三合一提示词:去掉 DESIGN CONTENT 四要素描述(设计已由设计稿提供)
4) 生图尺寸:全部改为读 config 不再硬编码(设计图 compose.design_size / 合成图 compose.size / 种草图 seed_shot.size)
2026-08-26 18:05:11 +08:00

237 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""构建并编译 LangGraph,提供 run_country() 入口。
图结构(线性流水线,节点全部带兜底):
START -> seed -> fetch -> filter -> score -> screen -> prompt_build
-> compose(生成纯印花设计稿 + 导出简报)-> product(底图/模特/三图合成/模板)
-> oss_upload(压缩 3:4 / ≥1340×1785 / <2MB + 上传阿里云 OSS-> END
"""
import time
from pathlib import Path
from typing import Any, Dict, Optional
from langgraph.graph import END, StateGraph
from graph.loader import build_country_config
from graph.nodes import (
compose_node,
fetch_node,
filter_node,
oss_upload_node,
product_node,
prompt_node,
score_node,
screen_node,
seed_node,
seed_shot_node,
template_export_node,
)
from graph.state import AgentState
def build_graph():
"""构建 StateGraph 并编译。"""
builder = StateGraph(AgentState)
builder.add_node("seed", seed_node)
builder.add_node("fetch", fetch_node)
builder.add_node("filter", filter_node)
builder.add_node("score", score_node)
builder.add_node("screen", screen_node)
builder.add_node("prompt_build", prompt_node)
builder.add_node("product", product_node)
builder.add_node("compose", compose_node)
builder.add_node("oss_upload", oss_upload_node)
builder.add_node("seed_shot", seed_shot_node)
builder.add_node("template_export", template_export_node)
builder.add_edge("__start__", "seed")
builder.add_edge("seed", "fetch")
builder.add_edge("fetch", "filter")
builder.add_edge("filter", "score")
builder.add_edge("score", "screen")
builder.add_edge("screen", "prompt_build")
builder.add_edge("prompt_build", "compose") # compose:生成纯印花设计稿(放前面)
builder.add_edge("compose", "product") # product:底图/模特/三图合成/模板
builder.add_edge("product", "oss_upload") # oss_upload:压缩 + 上传图床
builder.add_edge("oss_upload", "seed_shot") # seed_shot:种草图生成(模板+模特特征 yaml)→ 上传
builder.add_edge("seed_shot", "template_export") # template_export:最终结果导入商品上传模板
builder.add_edge("template_export", END)
return builder.compile()
def _pinterest_route(state: Dict[str, Any]) -> str:
"""图池路由:简报达标 → done;图池还有未消费图片 → analyze(继续分析,不搜索);
图池不足 → search(新一轮搜索);轮次耗尽 → done。"""
target = int(state.get("pinterest_target") or 0)
if target <= 0:
target = 1
briefs = state.get("briefs") or []
rounds = int(state.get("pinterest_rounds") or 0)
terms = state.get("pinterest_search_terms") or []
pcfg = (state.get("config") or {}).get("pinterest") or {}
max_rounds = int(pcfg.get("max_search_rounds") or 0)
if max_rounds <= 0:
max_rounds = max(target * 2, 5)
if len(briefs) >= target:
print(f"[pinterest_route] 简报已达目标 {len(briefs)}/{target},结束")
return "done"
# 图池还有未消费图片 → 继续分析(不搜索)
try:
from graph.pinterest import load_image_pool, load_used_images, pool_unused_images
pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "")
used = load_used_images(str(state.get("output_dir") or ""), state.get("country") or "")
unused = pool_unused_images(pool, used)
except Exception: # noqa: BLE001
unused = []
if unused:
print(f"[pinterest_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target}")
return "analyze"
# 图池不足 → 搜索
if rounds >= max_rounds:
print(f"[pinterest_route] 已达最大轮次 {max_rounds},简报 {len(briefs)}/{target},按现有结果继续")
return "done"
if not terms and rounds > 0:
print(f"[pinterest_route] 无可用搜索词,停止搜索(简报 {len(briefs)}/{target}")
return "done"
print(f"[pinterest_route] 图池不足,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target}")
return "search"
def build_pinterest_graph():
"""Pinterest 参考模式图(按需搜索循环 + 简报池并发生成):
pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze
→ [pinterest_route] 简报不足 → 回到 pinterest_search;达标 → pinterest_finalize
(排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export
"""
from graph.nodes import (
pinterest_analyze_node,
pinterest_scrape_node,
pinterest_search_node,
)
from graph.nodes.pinterest_finalize_node import pinterest_finalize_node
from graph.nodes.pinterest_init_node import pinterest_init_node
builder = StateGraph(AgentState)
builder.add_node("pinterest_init", pinterest_init_node)
builder.add_node("pinterest_search", pinterest_search_node)
builder.add_node("pinterest_scrape", pinterest_scrape_node)
builder.add_node("pinterest_analyze", pinterest_analyze_node)
builder.add_node("pinterest_finalize", pinterest_finalize_node)
builder.add_node("template_export", template_export_node)
builder.add_edge("__start__", "pinterest_init")
builder.add_edge("pinterest_init", "pinterest_search")
builder.add_edge("pinterest_search", "pinterest_scrape")
builder.add_edge("pinterest_scrape", "pinterest_analyze")
builder.add_conditional_edges("pinterest_analyze", _pinterest_route, {
"analyze": "pinterest_analyze", # 图池还有未消费图片 → 继续分析(不搜索)
"search": "pinterest_search", # 图池不足 → 新一轮搜索
"done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池)
})
builder.add_edge("pinterest_finalize", "template_export")
builder.add_edge("template_export", END)
return builder.compile()
def run_country(
country: str,
global_config: Dict[str, Any],
project_root: Path,
output_root: Optional[Path] = None,
base_image: Optional[str] = None,
task_timestamp: Optional[str] = None,
) -> Dict[str, Any]:
"""运行单个国家的完整流水线,返回最终 state(含 errors / stats / briefs)。
project_root:数据文件根(configs / prompts,打包后为 _MEIPASS 只读目录)。
output_root :产物输出根(默认=project_root;打包后传 exe 旁运行目录,
避免把 output/ 写进临时解压目录导致重启丢失)。
task_timestamp:任务时间戳(每次点击运行 = 一个任务);None 时自动生成。
"""
compiled = build_graph()
cc = build_country_config(global_config, country, project_root)
prompts_dir = project_root / "prompts" / country
cache_dir = (output_root or project_root) / "output" / country # 缓存/去重(根目录)
ts = task_timestamp or time.strftime("%Y%m%d_%H%M%S")
_base = ts
_i = 1
while (cache_dir / ts).exists(): # 时间戳文件夹唯一(同秒多任务防冲突/覆盖)
ts = f"{_base}_{_i}"
_i += 1
output_dir = cache_dir / ts # 本次任务产物(时间戳文件夹)
state: Dict[str, Any] = {
"country": country,
"config": global_config,
"country_config": cc,
"prompts_dir": str(prompts_dir),
"cache_dir": str(cache_dir),
"output_dir": str(output_dir),
"raw_rows": [],
"filtered_rows": [],
"scored_rows": [],
"screened": [],
"briefs": [],
"composite": [],
"designs": [],
"errors": [],
"stats": {},
"task_timestamp": ts, # 任务开始时间戳(OSS 路径段 / 产物文件夹名)
"oss_seq": 0, # 货号计数(000 起,最多 999)
}
if base_image:
state["base_image"] = base_image
result = compiled.invoke(state)
return result
def run_pinterest_ref(
country: str,
global_config: Dict[str, Any],
project_root: Path,
output_root: Optional[Path] = None,
task_timestamp: Optional[str] = None,
) -> Dict[str, Any]:
"""Pinterest 参考模式入口:独立于 Google Trends 的完整流程。
种子词 → LLM 搜索词(json_schema + 动态注入防重复)→ 爬图 → LLM 分析图片
→ 设计简报 → 设计稿 → 产品图 → 上传 → 种草图 → 模板导出。
参数语义与 run_country 一致(project_root=数据根,output_root=产物根)。
"""
compiled = build_pinterest_graph()
cc = build_country_config(global_config, country, project_root)
prompts_dir = project_root / "prompts" / country
cache_dir = (output_root or project_root) / "output" / country
ts = task_timestamp or time.strftime("%Y%m%d_%H%M%S")
_base = ts
_i = 1
while (cache_dir / ts).exists(): # 时间戳文件夹唯一(同秒多任务防冲突/覆盖)
ts = f"{_base}_{_i}"
_i += 1
output_dir = cache_dir / ts
state: Dict[str, Any] = {
"country": country,
"config": global_config,
"country_config": cc,
"prompts_dir": str(prompts_dir),
"cache_dir": str(cache_dir),
"output_dir": str(output_dir),
"briefs": [],
"composite": [],
"designs": [],
"errors": [],
"stats": {},
"task_timestamp": ts,
"oss_seq": 0,
# 按需搜索目标:简报数 = spu_tasks 数量(每款一个设计);无任务时回退 spu_count/1
"pinterest_target": len((global_config.get("product") or {}).get("spu_tasks") or [])
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1,
"pinterest_rounds": 0,
"pinterest_attempted": [],
}
return compiled.invoke(state)