v106-v109 童装支持 + 标题模板外部化 + 模板导出增强
- 新增男童/女童检测(gender_from_category 优先判童装)与童装场景图生成
(configs/kids_features.yaml:模特/场景/服装风格,同一商品固定同一组)
- 童装 SPU 字段映射:kids_type→SPU商品属性-类型、kids_age→适用年龄段、
target_audience 按性别映射、kids_type_map 女童「上衣」→「针织上衣」
- 标题生成提示词外部化:prompts/title_prompt_{1,2,3}.md + config.yaml 路由表
- 模板多站点匹配:经营站点可配多个,命中任意一个即匹配
- 种草图生成失败自动重试(seed_shot.retries,换场景/模特/风格)
- 模板导出新增 Preview 文件夹(成功产品 _composite.oss.jpg + result.xlsx)
- 修复 SKU 尺码未按从小到大排序(_size_rank 支持单一年龄码 6Y/10Y)
This commit is contained in:
+117
-21
@@ -44,9 +44,47 @@ def load_templates() -> List[Dict[str, str]]:
|
||||
for t in tpls if t.get("prompt")]
|
||||
|
||||
|
||||
_KIDS_FALLBACK = {
|
||||
"model_features": {
|
||||
"boy_kids": ["6岁亚洲小男孩,黑色短发,白净肌肤,圆脸大眼睛,活泼可爱"],
|
||||
"girl_kids": ["6岁亚洲小女孩,黑色长直发,白净肌肤,圆脸大眼睛,甜美可爱"],
|
||||
},
|
||||
"scene_features": {
|
||||
"boy_kids": ["阳光明媚的公园草坪,儿童游乐设施虚化背景,明亮自然光"],
|
||||
"girl_kids": ["阳光明媚的公园草坪,花朵与秋千虚化背景,明亮自然光"],
|
||||
},
|
||||
"style_features": {
|
||||
"boy_kids": ["童趣休闲风,活泼明快,适合日常玩耍"],
|
||||
"girl_kids": ["童趣甜美风,活泼可爱,适合日常玩耍"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _load_kids_features(section: str, gender: str) -> List[str]:
|
||||
"""从 kids_features.yaml 读取指定 section(model_features/scene_features/style_features)的 gender 特征列表。
|
||||
支持两种条目格式:字符串直接作为特征;dict 取 prompt 字段(场景特征为完整独立提示词时用)。"""
|
||||
data = _load_yaml("configs/kids_features.yaml")
|
||||
kf = (data.get("kids_features") or {}).get(section) or {}
|
||||
feats: List[str] = []
|
||||
for f in kf.get(gender, []):
|
||||
if isinstance(f, dict):
|
||||
p = f.get("prompt")
|
||||
if p and str(p).strip():
|
||||
feats.append(str(p).strip())
|
||||
elif f and str(f).strip():
|
||||
feats.append(str(f).strip())
|
||||
return feats
|
||||
|
||||
|
||||
def load_model_features(gender: Optional[str] = None) -> List[str]:
|
||||
"""模特特征列表(无配置时给内置兜底)。
|
||||
gender: "male"/"female" 时只返回对应性别;None/其他 返回全部(指定性别组为空时回退全部)。"""
|
||||
gender: "male"/"female" 时只返回对应性别;"boy_kids"/"girl_kids" 时从 kids_features.yaml 读取童装模特;
|
||||
None/其他 返回全部(指定性别组为空时回退全部)。"""
|
||||
if gender in ("boy_kids", "girl_kids"):
|
||||
feats = _load_kids_features("model_features", gender)
|
||||
if feats:
|
||||
return feats
|
||||
return list(_KIDS_FALLBACK["model_features"].get(gender, []))
|
||||
data = _load_yaml("configs/model_features.yaml")
|
||||
mf = data.get("model_features") or []
|
||||
feats: List[str] = []
|
||||
@@ -62,6 +100,16 @@ def load_model_features(gender: Optional[str] = None) -> List[str]:
|
||||
return feats
|
||||
|
||||
|
||||
def load_scene_features(gender: Optional[str] = None) -> List[str]:
|
||||
"""场景特征列表(童装专用,从 kids_features.yaml 读取;无配置给内置兜底)。"""
|
||||
if gender in ("boy_kids", "girl_kids"):
|
||||
feats = _load_kids_features("scene_features", gender)
|
||||
if feats:
|
||||
return feats
|
||||
return list(_KIDS_FALLBACK["scene_features"].get(gender, []))
|
||||
return []
|
||||
|
||||
|
||||
def read_template_category(template_path: str) -> str:
|
||||
"""读取模版「类目」表头对应的值(如 服装、鞋靴和珠宝饰品>男士时尚>男装>男装上衣、T恤、衬衫>男装T恤)。
|
||||
遍历所有 sheet(类目表头可能在「模版」等 sheet),找到即返回其下一行同列值。"""
|
||||
@@ -85,7 +133,13 @@ def read_template_category(template_path: str) -> str:
|
||||
|
||||
|
||||
def gender_from_category(category: str) -> Optional[str]:
|
||||
"""类目含「男」→ male;含「女」→ female;都不含 → None(全部随机)。"""
|
||||
"""类目含「男童」→ boy_kids;含「女童」→ girl_kids;含「男」→ male;含「女」→ female;都不含 → None。
|
||||
|
||||
顺序必须先判「男童/女童」再判「男/女」,因为「男童」也含「男」、「女童」也含「女」。"""
|
||||
if "男童" in category:
|
||||
return "boy_kids"
|
||||
if "女童" in category:
|
||||
return "girl_kids"
|
||||
if "男" in category:
|
||||
return "male"
|
||||
if "女" in category:
|
||||
@@ -93,8 +147,15 @@ def gender_from_category(category: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def load_style_features() -> List[str]:
|
||||
"""服装风格列表(style_features.yaml,随机取一条替换 [服装风格];无配置时内置兜底)。"""
|
||||
def load_style_features(gender: Optional[str] = None) -> List[str]:
|
||||
"""服装风格列表(童装从 kids_features.yaml 读取;无配置回退 style_features.yaml 全量)。"""
|
||||
if gender in ("boy_kids", "girl_kids"):
|
||||
feats = _load_kids_features("style_features", gender)
|
||||
if feats:
|
||||
return feats
|
||||
feats = list(_KIDS_FALLBACK["style_features"].get(gender, []))
|
||||
if feats:
|
||||
return feats
|
||||
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:
|
||||
@@ -104,35 +165,47 @@ def load_style_features() -> List[str]:
|
||||
|
||||
|
||||
def render_prompt(template_prompt: str, cn_title: str, material: str, model_feature: str,
|
||||
style_feature: str = "") -> str:
|
||||
"""占位替换:[商品名称]/[材质]/[模特特征]/[服装风格]"""
|
||||
style_feature: str = "", scene_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 "日常休闲风")
|
||||
scene = (scene_feature or "").strip()
|
||||
if "[场景]" in template_prompt:
|
||||
out = out.replace("[场景]", scene or "明亮干净的室内摄影棚")
|
||||
elif scene:
|
||||
out = f"{out} 场景:{scene}"
|
||||
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]:
|
||||
gender: Optional[str] = None, retries: int = 3) -> List[str]:
|
||||
"""生成 count 张种草图(img2img,图1=合成图)。返回产物路径列表。
|
||||
size: 种草图统一 1536x2048(与合成图一致)。
|
||||
prefix: 货号前缀(对应产品货号,命名 {prefix}_{随机4位}.png,不覆盖旧文件)。
|
||||
gender: "male"/"female" 时只从对应性别模特特征随机;None 全部随机。
|
||||
占位符 [商品名称]/[材质]/[模特特征]/[服装风格] 均随机组合(模板/模特/服装风格各随机取一条)。"""
|
||||
gender: "male"/"female" 时只从对应性别模特特征随机;"boy_kids"/"girl_kids" 时从 kids_features.yaml
|
||||
读取童装模特/场景/服装风格;None 全部随机。
|
||||
retries: 单张失败重试次数(图像 API 偶发安全拦截 400 时自动换一组场景/模特/风格重试)。
|
||||
占位符 [商品名称]/[材质]/[模特特征]/[服装风格]/[场景] 均随机组合(模板/模特/服装风格/场景各随机取一条)。
|
||||
童装(boy_kids/girl_kids):场景特征为完整独立提示词,直接作为模板使用(不套用 seed_shot_templates.yaml),
|
||||
且同一商品全套图固定同一组 [模特特征]+[场景]+[服装风格],系列图调性统一(失败重试时换一组)。"""
|
||||
templates = load_templates()
|
||||
features = load_model_features(gender=gender)
|
||||
style_features = load_style_features()
|
||||
style_features = load_style_features(gender=gender)
|
||||
scene_features = load_scene_features(gender=gender)
|
||||
is_kids = gender in ("boy_kids", "girl_kids")
|
||||
out = Path(out_dir)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
paths: List[str] = []
|
||||
# 童装:全套图固定同一组 模特+场景+服装风格(系列图调性统一;失败重试时换一组)
|
||||
kids_feat = random.choice(features) if is_kids else ""
|
||||
kids_style = random.choice(style_features) if is_kids else ""
|
||||
kids_scene = random.choice(scene_features) if (is_kids and scene_features) else ""
|
||||
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}"
|
||||
@@ -140,11 +213,34 @@ def generate_seed_shots(image_backend, base_image: str, cn_title: str, material:
|
||||
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}")
|
||||
last_err: Optional[Exception] = None
|
||||
for attempt in range(1, retries + 1):
|
||||
if is_kids and kids_scene:
|
||||
# 童装:场景完整提示词直接作为模板,仅填占位符,不追加任何其他模板
|
||||
prompt = render_prompt(kids_scene, cn_title, material, kids_feat, kids_style)
|
||||
tpl_name = "kids_scene"
|
||||
feat, style_feat = kids_feat, kids_style
|
||||
else:
|
||||
tpl = random.choice(templates)
|
||||
feat = random.choice(features)
|
||||
style_feat = random.choice(style_features)
|
||||
scene_feat = random.choice(scene_features) if scene_features else ""
|
||||
prompt = render_prompt(tpl["prompt"], cn_title, material, feat, style_feat, scene_feat)
|
||||
tpl_name = tpl["name"]
|
||||
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]}…)")
|
||||
break
|
||||
except Exception as e: # noqa: BLE001
|
||||
last_err = e
|
||||
print(f"[seed_shot] 种草图 {i + 1} 第 {attempt}/{retries} 次失败: {e}")
|
||||
# 重试换一组(童装换场景/模特/风格;成人换模板/特征)
|
||||
if is_kids and scene_features:
|
||||
kids_scene = random.choice(scene_features)
|
||||
kids_feat = random.choice(features)
|
||||
kids_style = random.choice(style_features)
|
||||
else:
|
||||
print(f"[seed_shot] 种草图 {i + 1} 重试 {retries} 次仍失败: {last_err}")
|
||||
return paths
|
||||
|
||||
Reference in New Issue
Block a user