Files
pod_trend_agent/graph/seed_shot.py
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

151 lines
7.3 KiB
Python
Raw Permalink 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.
"""种草图(Seed Shot)生成。
- 模板:configs/seed_shot_templates.yaml(可自定义,占位符 [商品名称]/[材质]/[模特特征])
- 模特特征:configs/model_features.yaml(可自定义,随机取一条)
- 生成:以 product 合成图(图1)为参考,img2img 生成 N 张种草图(保留衣服外观、换场景/模特)
- 占位替换:[商品名称]→cn_title(缺省回退 topic);[材质]→SPU.material[模特特征]→随机
"""
import random
from pathlib import Path
from typing import Any, Dict, List, Optional
import yaml
from graph.paths import project_root
def _load_yaml(rel: str) -> Dict[str, Any]:
for root in (project_root(),):
p = root / rel
if p.exists():
try:
return yaml.safe_load(p.read_text(encoding="utf-8")) or {}
except Exception as e: # noqa: BLE001
print(f"[seed_shot] 读取 {rel} 失败: {e}")
return {}
def load_templates() -> List[Dict[str, str]]:
"""种草图提示词模板列表(无配置时给内置兜底)。"""
data = _load_yaml("configs/seed_shot_templates.yaml")
tpls = data.get("seed_shot_templates") or []
if not tpls:
tpls = [{
"name": "default",
"prompt": (
"【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、"
"印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、"
"重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上。"
"全身动态抓拍构图,行走在阳光斑驳的城市林荫道上,微微低头微笑,凸显[材质]的透气与百搭。"
"徕卡Q2摄影质感,高对比度色彩,35mm镜头,f/1.7大光圈,8k分辨率。"
),
}]
return [{"name": str(t.get("name", "default")), "prompt": str(t.get("prompt", ""))}
for t in tpls if t.get("prompt")]
def load_model_features(gender: Optional[str] = None) -> List[str]:
"""模特特征列表(无配置时给内置兜底)。
gender: "male"/"female" 时只返回对应性别;None/其他 返回全部(指定性别组为空时回退全部)。"""
data = _load_yaml("configs/model_features.yaml")
mf = data.get("model_features") or []
feats: List[str] = []
if isinstance(mf, dict):
if gender and gender in mf:
feats = [str(f) for f in mf[gender] if str(f).strip()]
if not feats:
feats = [str(f) for g in mf.values() for f in g if str(f).strip()]
else:
feats = [str(f) for f in mf if str(f).strip()]
if not feats:
feats = ["20岁清新少女,素颜通透感", "25岁都市职场女性,干练气质"]
return feats
def read_template_category(template_path: str) -> str:
"""读取模版「类目」表头对应的值(如 服装、鞋靴和珠宝饰品>男士时尚>男装>男装上衣、T恤、衬衫>男装T恤)。
遍历所有 sheet(类目表头可能在「模版」等 sheet),找到即返回其下一行同列值。"""
try:
import openpyxl
wb = openpyxl.load_workbook(template_path, data_only=True, read_only=True)
try:
for ws in wb.worksheets:
rows = [r for r in ws.iter_rows(min_row=1, max_row=5, values_only=True)]
for ri, row in enumerate(rows):
for ci, v in enumerate(row):
if v is not None and str(v).strip() == "类目":
if ri + 1 < len(rows):
val = rows[ri + 1][ci]
return str(val or "").strip()
finally:
wb.close()
except Exception as e: # noqa: BLE001
print(f"[seed_shot] 读取模版类目失败: {e}")
return ""
def gender_from_category(category: str) -> Optional[str]:
"""类目含「男」→ male;含「女」→ female;都不含 → None(全部随机)。"""
if "男" in category:
return "male"
if "女" in category:
return "female"
return None
def load_style_features() -> List[str]:
"""服装风格列表(style_features.yaml,随机取一条替换 [服装风格];无配置时内置兜底)。"""
data = _load_yaml("configs/style_features.yaml")
feats = [str(f) for f in (data.get("style_features") or []) if str(f).strip()]
if not feats:
feats = ["极简基础款风格,干净纯粹,无过多繁复装饰",
"日系City Boy/Girl风,微宽松版型,注重舒适度与层次感"]
return feats
def render_prompt(template_prompt: str, cn_title: str, material: str, model_feature: str,
style_feature: str = "") -> str:
"""占位替换:[商品名称]/[材质]/[模特特征]/[服装风格]"""
out = template_prompt.replace("[商品名称]", (cn_title or "").strip() or "这件衣服")
out = out.replace("[材质]", (material or "").strip() or "面料")
out = out.replace("[模特特征]", (model_feature or "").strip() or "模特")
out = out.replace("[服装风格]", (style_feature or "").strip() or "日常休闲风")
return out
def generate_seed_shots(image_backend, base_image: str, cn_title: str, material: str,
count: int, out_dir: str, negative: str = "",
size: str = "1536x2048", prefix: str = "",
gender: Optional[str] = None) -> List[str]:
"""生成 count 张种草图(img2img,图1=合成图)。返回产物路径列表。
size: 种草图统一 1536x2048(与合成图一致)。
prefix: 货号前缀(对应产品货号,命名 {prefix}_{随机4位}.png,不覆盖旧文件)。
gender: "male"/"female" 时只从对应性别模特特征随机;None 全部随机。
占位符 [商品名称]/[材质]/[模特特征]/[服装风格] 均随机组合(模板/模特/服装风格各随机取一条)。"""
templates = load_templates()
features = load_model_features(gender=gender)
style_features = load_style_features()
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
paths: List[str] = []
for i in range(count):
tpl = random.choice(templates)
feat = random.choice(features)
style_feat = random.choice(style_features)
prompt = render_prompt(tpl["prompt"], cn_title, material, feat, style_feat)
# 命名:{货号}_{随机4位}.png(按货号命名,随机4位避免自增序号/覆盖)
while True:
rand4 = f"{random.randint(0, 9999):04d}"
out_path = str(out / f"{prefix}_{rand4}.png" if prefix
else out / f"seed_{rand4}.png")
if not Path(out_path).exists():
break
try:
image_backend.print(prompt, base_image, out_path, negative, size=size)
paths.append(out_path)
print(f"[seed_shot] 已生成种草图 {i + 1}/{count}: {out_path}"
f"(模板={tpl['name']},模特={feat[:14]}…,风格={style_feat[:14]}…)")
except Exception as e: # noqa: BLE001
print(f"[seed_shot] 种草图 {i + 1} 生成失败: {e}")
return paths