Files
pod_trend_agent/graph/nodes/seed_shot_node.py
T
3218485270 f493bde8a9 POD 趋势感知 Agent:缓存热点模式 + 三图合成 + 热点去重/风格去重 + review 兜底
- 缓存热点批量流程(有采集缓存不触发 Google)
- 简报不足直接从采集缓存生成(轻量补齐)
- 三图合成(模特/印花/底图)+ 底图压缩 <2MB
- 热点去重→风格去重自动切换 + 不适合类目 review 兜底
- 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述)
- 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
2026-08-22 14:14:01 +08:00

134 lines
6.0 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.
"""节点 8/8:种草图生成(seed_shot)——在 oss_upload 之后。
对每个 product 的合成图(图1),按 seed_shot_templates.yaml 模板 + model_features.yaml 随机模特特征
生成 N 张种草图(config.seed_shot.count,默认 1):
- [商品名称] ← product 的 cn_title(上一节点多模态生成)
- [材质] ← 数据库 SPU.material 字段
- [模特特征] ← model_features.yaml 随机一条
种草图同样压缩上传到 OSS(货号计数与 oss_upload 共用 state["oss_seq"] 续接)。
未配置图像后端 / 无合成图 / count=0 时跳过,不中断。
"""
import time
from pathlib import Path
from typing import Any, Dict, List
from graph.validate import with_fallback
@with_fallback("seed_shot")
def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
products: List[Dict[str, Any]] = state.get("product") or []
config = state["config"] or {}
country = state.get("country", "")
output_dir = Path(state["output_dir"])
ss_cfg = config.get("seed_shot") or {}
count = int(ss_cfg.get("count", 1))
if not bool(ss_cfg.get("enabled", True)) or count <= 0 or not products:
return {"seed_shots": [], "stats": state.get("stats") or {}}
# 图像后端(复用 compose 配置)
compose_cfg = config.get("compose") or {}
ib = None
if compose_cfg.get("backend"):
from graph.backends import get_image_backend
try:
ib = get_image_backend(compose_cfg["backend"])
if ib is not None:
ib.bind_config(compose_cfg)
except Exception as e: # noqa: BLE001
print(f"[seed_shot] 图像后端不可用: {e}")
if ib is None:
print("[seed_shot] 未配置 compose.backendopenai/mock),跳过种草图生成")
return {"seed_shots": [], "stats": state.get("stats") or {}}
# 材质映射:db SPU.material(清洗换行)
material_map: Dict[str, str] = {}
try:
from graph.product import list_spus
import yaml
dbp = (config.get("product") or {}).get("db_path", "db/spu_sku.db")
p = Path(dbp)
if not p.is_absolute():
from graph.paths import project_root, runtime_root
for root in (runtime_root(), project_root()):
if (root / p).exists():
p = root / p
break
for s in list_spus(str(p)):
m = " ".join(str(s.get("material", "")).replace("\r", " ").replace("\n", " ").split())
material_map[s["code"]] = m
except Exception as e: # noqa: BLE001
print(f"[seed_shot] 材质读取失败(用空): {e}")
from graph.seed_shot import generate_seed_shots
from graph.oss_upload import build_oss_key, compress_for_oss, upload_to_oss
from graph.nodes.oss_upload_node import _gen_rand4, MAX_CODE
ts = str(state.get("task_timestamp") or time.strftime("%Y%m%d%H%M%S"))
prefix = str(((config.get("product") or {}).get("code_prefix")) or "DG").strip()
seq = int(state.get("oss_seq") or 0)
oss_cfg = config.get("oss") or {}
oss_enabled = bool(oss_cfg.get("enabled", True)) and bool(oss_cfg.get("oss_bucket"))
all_shots: List[Dict[str, Any]] = []
shot_dir = output_dir / "seed_shots"
shot_dir.mkdir(parents=True, exist_ok=True)
import concurrent.futures
import threading as _th
seq_lock = _th.Lock() # seq(货号计数)跨线程共享,需加锁
def _shot_one(r: Dict[str, Any]):
"""单个产品的种草图生成+上传(每产品独立线程)。"""
nonlocal seq
base = r.get("composite_path") or r.get("printed_path")
if not base or not Path(base).exists():
print(f"[seed_shot] {r.get('spu_code', '')} 无合成图,跳过种草图")
return None
cn = (r.get("cn_title") or "").strip() or r.get("topic", "")
material = material_map.get(r.get("spu_code", ""), "")
paths = generate_seed_shots(ib, base, cn, material, count, str(shot_dir),
r.get("composite_negative", ""),
size=str((config.get("seed_shot") or {}).get("size") or "1504x2000"),
prefix=r.get("img_code") or r.get("oss_code") or "")
if not paths:
return None
r["seed_shot_paths"] = paths
urls: List[str] = []
for pth in paths:
with seq_lock:
if seq >= MAX_CODE:
print(f"[seed_shot] 货号计数达上限 999,停止上传种草图")
break
code = f"{prefix}{seq:03d}"
seq += 1
if oss_enabled:
try:
compressed = compress_for_oss(pth, str(Path(pth).with_suffix(".oss.jpg")))
url = upload_to_oss(oss_cfg, compressed,
build_oss_key(country, ts, code, _gen_rand4()))
if url:
urls.append(url)
r["seed_shot_urls"] = urls
except Exception as e: # noqa: BLE001
print(f"[seed_shot] 种草图上传失败 {pth}: {e}")
else:
print(f"[seed_shot] oss 未启用,仅本地保存: {pth}")
return {"spu_code": r.get("spu_code"), "sku_code": r.get("sku_code"),
"paths": paths, "urls": urls}
# 并发:每个产品一个独立线程(默认);config.seed_shot.concurrency 可覆盖
seed_concurrency = int((config.get("seed_shot") or {}).get("concurrency") or 0) or len(products) or 1
if len(products) > 1:
print(f"[seed_shot] 并发 {seed_concurrency} 生成种草图({len(products)} 个产品)")
with concurrent.futures.ThreadPoolExecutor(max_workers=seed_concurrency) as _ex:
for item in _ex.map(_shot_one, products):
if item:
all_shots.append(item)
stats = dict(state.get("stats") or {})
stats["seed_shot"] = {"count": len(all_shots), "seq": seq}
return {"seed_shots": all_shots, "product": products, "oss_seq": seq, "stats": stats}