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,6 +117,7 @@ pod_trend_agent/
|
|||||||
│ ├── countries/ # 各国 Google Trends 种子词(US|GB|JP|AU|MX|DE|BR|SA|PL|ES|IT|CA|KR.yaml)
|
│ ├── countries/ # 各国 Google Trends 种子词(US|GB|JP|AU|MX|DE|BR|SA|PL|ES|IT|CA|KR.yaml)
|
||||||
│ ├── pinterest/ # 各国 Pinterest 种子词池(<CC>.yaml)
|
│ ├── pinterest/ # 各国 Pinterest 种子词池(<CC>.yaml)
|
||||||
│ ├── model_features.yaml # 模特特征库(按性别分组 female/male,种草图用)
|
│ ├── model_features.yaml # 模特特征库(按性别分组 female/male,种草图用)
|
||||||
|
│ ├── kids_features.yaml # ★ 童装特征库(男童/女童:模特、场景、服装风格,种草图用)
|
||||||
│ ├── seed_shot_templates.yaml # 种草图提示词模板
|
│ ├── seed_shot_templates.yaml # 种草图提示词模板
|
||||||
│ └── style_features.yaml
|
│ └── style_features.yaml
|
||||||
├── prompts/<country>/ # ★ 每个国家不同的提示词单独文件夹
|
├── prompts/<country>/ # ★ 每个国家不同的提示词单独文件夹
|
||||||
@@ -126,6 +127,9 @@ pod_trend_agent/
|
|||||||
├── prompts/pinterest_analyze_user.md # 多模态分析用户提示词,可配置
|
├── prompts/pinterest_analyze_user.md # 多模态分析用户提示词,可配置
|
||||||
├── prompts/custom_analyze_system.md # 自定义模式多模态分析系统提示词(独立于 Pinterest)
|
├── prompts/custom_analyze_system.md # 自定义模式多模态分析系统提示词(独立于 Pinterest)
|
||||||
├── prompts/custom_analyze_user.md # 自定义模式多模态分析用户提示词(独立于 Pinterest)
|
├── prompts/custom_analyze_user.md # 自定义模式多模态分析用户提示词(独立于 Pinterest)
|
||||||
|
├── prompts/title_prompt_1.md # ★ 标题生成提示词(英语市场 US/GB/AU/MX)
|
||||||
|
├── prompts/title_prompt_2.md # ★ 标题生成提示词(日本市场 JP)
|
||||||
|
├── prompts/title_prompt_3.md # ★ 标题生成提示词(西班牙市场 ES)
|
||||||
├── graph/
|
├── graph/
|
||||||
│ ├── state.py # AgentState(共享状态)
|
│ ├── state.py # AgentState(共享状态)
|
||||||
│ ├── validate.py # with_fallback 兜底 + 数据校验
|
│ ├── validate.py # with_fallback 兜底 + 数据校验
|
||||||
@@ -158,6 +162,7 @@ pod_trend_agent/
|
|||||||
├── design_briefs.json/md # 设计简报(含 image_prompt / composite_prompt)
|
├── design_briefs.json/md # 设计简报(含 image_prompt / composite_prompt)
|
||||||
├── designs/ # 纯印花设计稿
|
├── designs/ # 纯印花设计稿
|
||||||
├── product/ # 三合一合成图 + 已填写商品模板 <SKU>_已填写.xlsx
|
├── product/ # 三合一合成图 + 已填写商品模板 <SKU>_已填写.xlsx
|
||||||
|
├── Preview/ # ★ 成功导入模板产品的 _composite.oss.jpg 压缩图 + result.xlsx(货号|中文标题|英文标题)
|
||||||
└── report.md # 各阶段统计 + 兜底错误记录
|
└── report.md # 各阶段统计 + 兜底错误记录
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -204,6 +209,18 @@ Pinterest 参考模式与自定义图片模式的多模态分析提示词已**
|
|||||||
`image_prompt` 二段式结构 / `suitable_for_print` 判定标准等约束(多模态分析不再产出
|
`image_prompt` 二段式结构 / `suitable_for_print` 判定标准等约束(多模态分析不再产出
|
||||||
`negative_prompt`,生图负向约束由固定 `print_suffix` 承担)。
|
`negative_prompt`,生图负向约束由固定 `print_suffix` 承担)。
|
||||||
|
|
||||||
|
## 标题生成提示词(可配置)
|
||||||
|
|
||||||
|
标题生成提示词已**外部化为独立文件**,无需改代码即可调优:
|
||||||
|
|
||||||
|
- `prompts/title_prompt_1.md`——英语市场(US/GB/AU/MX),输出 `en_title` + `cn_title`;
|
||||||
|
- `prompts/title_prompt_2.md`——日本市场(JP),输出 `en_title` + `cn_title` + `ja_title`;
|
||||||
|
- `prompts/title_prompt_3.md`——西班牙市场(ES),输出 `es_title` + `cn_title`。
|
||||||
|
|
||||||
|
**路由表**在 `config.yaml` 的 `title_templates.route`(国家 → 模板编号),新增国家/改路由只需编辑
|
||||||
|
config.yaml。模板内 `{year}`/`{month}`/`{season}` 占位符运行时自动替换为当前时间;md/yaml 缺失时
|
||||||
|
回退代码内置默认模板,标题生成不中断。
|
||||||
|
|
||||||
## 生成可靠性 / 错误处理
|
## 生成可靠性 / 错误处理
|
||||||
|
|
||||||
- **致命 503(图像服务不可用,如 "No available compatible accounts")**:端到端识别
|
- **致命 503(图像服务不可用,如 "No available compatible accounts")**:端到端识别
|
||||||
@@ -231,10 +248,27 @@ Pinterest 参考模式与自定义图片模式的多模态分析提示词已**
|
|||||||
|
|
||||||
`configs/model_features.yaml` 的模特特征按性别分组(`female` / `male`)。种草图生成时读取商品上传
|
`configs/model_features.yaml` 的模特特征按性别分组(`female` / `male`)。种草图生成时读取商品上传
|
||||||
模板的「类目」表头值:
|
模板的「类目」表头值:
|
||||||
|
- 类目含「男童」→ 固定从 **boy_kids** 组随机取模特(童装)
|
||||||
|
- 类目含「女童」→ 固定从 **girl_kids** 组随机取模特(童装)
|
||||||
- 类目含「男」(如 `男士时尚>男装>男装T恤`)→ 固定从 **male** 组随机取模特
|
- 类目含「男」(如 `男士时尚>男装>男装T恤`)→ 固定从 **male** 组随机取模特
|
||||||
- 类目含「女」→ 固定从 **female** 组随机取模特
|
- 类目含「女」→ 固定从 **female** 组随机取模特
|
||||||
- 都不含 → 男女混合随机
|
- 都不含 → 男女混合随机
|
||||||
|
|
||||||
|
**判定顺序**:先判「男童/女童」再判「男/女」(因为"男童"也含"男"、"女童"也含"女")。
|
||||||
|
|
||||||
|
## 童装场景图生成(男童 / 女童)
|
||||||
|
|
||||||
|
检测到模板类目为男童/女童时,种草图**不再套用** `seed_shot_templates.yaml`,而是直接使用
|
||||||
|
`configs/kids_features.yaml` 里配置的**完整场景提示词**(同一商品全套图固定同一组 模特+场景+服装风格):
|
||||||
|
|
||||||
|
- `model_features.boy_kids` / `model_features.girl_kids`——童装模特特征
|
||||||
|
- `scene_features.boy_kids` / `scene_features.girl_kids`——童装场景完整提示词(支持
|
||||||
|
`[商品名称]`/`[材质]`/`[模特特征]`/`[服装风格]` 占位符)
|
||||||
|
- `style_features.boy_kids` / `style_features.girl_kids`——童装服装风格
|
||||||
|
|
||||||
|
**种草图重试**:`config.yaml` 的 `seed_shot.retries`(默认 3)——生成失败(如图像 API 安全策略拦截)
|
||||||
|
时自动换一组 场景/模特/风格 重试,全部失败则返回空、不中断流水线;成人模式失败时换模板/特征重试。
|
||||||
|
|
||||||
## 生图尺寸(读 config,不硬编码)
|
## 生图尺寸(读 config,不硬编码)
|
||||||
|
|
||||||
| 图片类型 | 配置项 | 默认 |
|
| 图片类型 | 配置项 | 默认 |
|
||||||
@@ -273,6 +307,16 @@ Pinterest 参考模式与自定义图片模式的多模态分析提示词已**
|
|||||||
`occasion→场合`、`scene→场景`、`hemline_shape→下摆形状`,各字段独立不覆盖;
|
`occasion→场合`、`scene→场景`、`hemline_shape→下摆形状`,各字段独立不覆盖;
|
||||||
- **季节映射(女装)**:`spu.season` 的「四季」→「ALL/全球/所有」(`config.yaml` 的 `season_map.female` 可配置);
|
- **季节映射(女装)**:`spu.season` 的「四季」→「ALL/全球/所有」(`config.yaml` 的 `season_map.female` 可配置);
|
||||||
- **印花图案映射(女装)**:`spu.pattern` 为「印花」时 →「卡通」(`pattern_map` 可配置);
|
- **印花图案映射(女装)**:`spu.pattern` 为「印花」时 →「卡通」(`pattern_map` 可配置);
|
||||||
|
- **适用人群映射(童装)**:`spu.target_audience` 按童装性别映射(`target_audience_map.kids` 可配置),
|
||||||
|
如女童模板「男童」→「女童」、男童模板「女童」→「男童」;
|
||||||
|
- **童装字段映射**:检测到男童/女童模板时,`spu.kids_type` →「SPU商品属性-类型」、`spu.kids_age` →「适用年龄段」;
|
||||||
|
**kids_type 值映射**:`kids_type_map.girl_kids`(可配置)仅女童模板生效,如「上衣」→「针织上衣」,男童保留原值;
|
||||||
|
- **SKU 尺码升序排序**:写入模板前按 `sku.size` 从小到大排序(`_size_rank` 支持数字码/字母码/童装年龄码
|
||||||
|
6Y/10Y/1-2Y/均码等),保证每个 SPU 块内尺码有序,不受 db 存储顺序影响;
|
||||||
|
- **多站点模板匹配**:模板「经营站点」表头下可配置多个站点(逐行读取),当前国家命中其中任意一个即视为
|
||||||
|
匹配,不再只认第一个站点;
|
||||||
|
- **Preview 文件夹**:模板导出成功后生成 `output/<国家>/<ts>/Preview/`——复制成功导入模板产品的
|
||||||
|
`_composite.oss.jpg` 压缩图 + 生成 `result.xlsx`(货号 | 中文标题 | 英文标题),仅含成功产品;
|
||||||
- **详情图文**:由全部主图 + 种草图组成(不再拼接 img_url_2);
|
- **详情图文**:由全部主图 + 种草图组成(不再拼接 img_url_2);
|
||||||
- 规格类型2 = `{size}*{color}`;币种=CNY;发货仓1~N 取模板顶头按「、」分隔,库存均 200;
|
- 规格类型2 = `{size}*{color}`;币种=CNY;发货仓1~N 取模板顶头按「、」分隔,库存均 200;
|
||||||
- 模板文件被占用(打开中)时自动换名 `<SKU>_已填写_N.xlsx`,不中断导出;
|
- 模板文件被占用(打开中)时自动换名 `<SKU>_已填写_N.xlsx`,不中断导出;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 467 KiB |
+39
-68
@@ -185,6 +185,22 @@ pattern_map:
|
|||||||
female:
|
female:
|
||||||
印花: 卡通
|
印花: 卡通
|
||||||
|
|
||||||
|
# 适用人群值映射表(童装模板专用):key=db target_audience 值,value=模板下拉框精确选项。
|
||||||
|
# 女童模板遇到「男童」→「女童」;男童模板遇到「女童」→「男童」(内置默认,可在此覆盖/新增)。
|
||||||
|
# 成人男/女装模板保留 db 原值(不查此表,不在此配置)。
|
||||||
|
target_audience_map:
|
||||||
|
kids:
|
||||||
|
girl_kids:
|
||||||
|
男童: 女童
|
||||||
|
boy_kids:
|
||||||
|
女童: 男童
|
||||||
|
|
||||||
|
# kids_type 值映射表(童装模板专用):key=db kids_type 值,value=模板「SPU商品属性-类型」下拉框精确选项。
|
||||||
|
# 仅女童模板映射(女童遇到「上衣」→「针织上衣」);男童模板保留 db 原值(不查此表)。
|
||||||
|
kids_type_map:
|
||||||
|
girl_kids:
|
||||||
|
上衣: 针织上衣
|
||||||
|
|
||||||
# 合规黑名单(全局,生效于所有国家;各国专属侵权盲区写在 prompts/<country>/aesthetics.yaml 的 extra_blacklist)
|
# 合规黑名单(全局,生效于所有国家;各国专属侵权盲区写在 prompts/<country>/aesthetics.yaml 的 extra_blacklist)
|
||||||
blacklist:
|
blacklist:
|
||||||
- disney
|
- disney
|
||||||
@@ -313,6 +329,7 @@ oss:
|
|||||||
seed_shot:
|
seed_shot:
|
||||||
enabled: true
|
enabled: true
|
||||||
count: 1 # 每个产品选取几张种草图(从三合一主图中随机取,传入数量即可)
|
count: 1 # 每个产品选取几张种草图(从三合一主图中随机取,传入数量即可)
|
||||||
|
retries: 3 # 单张失败重试次数(图像 API 偶发安全拦截 400 时自动换一组场景/模特/风格重试)
|
||||||
size: "1536x2048" # 种草图生成尺寸(3:4,与合成图一致)
|
size: "1536x2048" # 种草图生成尺寸(3:4,与合成图一致)
|
||||||
|
|
||||||
# —— 第四阶段 product:热点 → SPU/颜色选品 → 底图 → 三图合成(图1模特+图2设计稿+图3底图)→ 模板 ——
|
# —— 第四阶段 product:热点 → SPU/颜色选品 → 底图 → 三图合成(图1模特+图2设计稿+图3底图)→ 模板 ——
|
||||||
@@ -326,80 +343,18 @@ product:
|
|||||||
basemap_dir: "basemap"
|
basemap_dir: "basemap"
|
||||||
material_library_dir: "material_library"
|
material_library_dir: "material_library"
|
||||||
model_category: "T-shirt" # 备用模特目录优先品类;为空/无图时取 material_library 第一个有图的子目录;SPU.mark==1 才启用试穿/平铺合成
|
model_category: "T-shirt" # 备用模特目录优先品类;为空/无图时取 material_library 第一个有图的子目录;SPU.mark==1 才启用试穿/平铺合成
|
||||||
# 三方关系映射:SPU.mark → material_library 下的图文件夹 + 对应提示词。
|
# 三方关系映射:SPU.mark → material_library 下的图文件夹。
|
||||||
# 每个 mark 可配置两个图源(模特图文件夹 / 平铺图文件夹),运行时在「有图的文件夹」间随机抽图:
|
# 每个 mark 可配置两个图源(模特图文件夹 / 平铺图文件夹),运行时在「有图的文件夹」间随机抽图:
|
||||||
# - 抽到 model_dir(模特图)→ 用 model_prompt(模特三图:图1模特+图2印花+图3底图)
|
# - 抽到 model_dir(模特图)→ 用模特三图提示词(图1模特+图2印花+图3底图)
|
||||||
# - 抽到 flat_dir(平铺图) → 用 flat_prompt(平铺三图:图1平铺实拍+图2印花+图3底图)
|
# - 抽到 flat_dir(平铺图) → 用平铺三图提示词(图1平铺实拍+图2印花+图3底图)
|
||||||
# model_prompt/flat_prompt 留空则用内置默认提示词(MODEL_WEAR_PROMPT / FLAT_LAY_PROMPT)。
|
# 三图合成提示词模板已迁移到独立 md 文件(不再在此配置):
|
||||||
|
# prompts/model_prompt.md 模特三图提示词;prompts/flat_prompt.md 平铺三图提示词
|
||||||
|
# 读取优先级:运行根(exe 旁)prompts/ > 数据根 prompts/;文件缺失/留空回退代码内置默认。
|
||||||
# 该映射完全可配置、不写死在代码里。
|
# 该映射完全可配置、不写死在代码里。
|
||||||
mark_dirs:
|
mark_dirs:
|
||||||
"1":
|
"1":
|
||||||
model_dir: "模特图" # 模特图文件夹名(material_library 下)
|
model_dir: "模特图" # 模特图文件夹名(material_library 下)
|
||||||
flat_dir: "平铺图" # 平铺图文件夹名(material_library 下)
|
flat_dir: "平铺图" # 平铺图文件夹名(material_library 下)
|
||||||
model_prompt: "" # 留空=内置模特三图提示词 MODEL_WEAR_PROMPT
|
|
||||||
flat_prompt: |-
|
|
||||||
|
|
||||||
**你是一个专业的电商AI视觉合成工具**,核心任务是执行"高保真印花与色彩移植/印花替换"。
|
|
||||||
|
|
||||||
## 1. 图片角色定义
|
|
||||||
* **图1 (基底图):** 衣服平铺实拍图。
|
|
||||||
* **功能:** 仅提供版型、轮廓、褶皱形态、光影结构、拍摄角度与构图。
|
|
||||||
* **限制:** 输出必须保留图1的摆放角度、背景和光影逻辑,但**彻底摒弃**其原有的面料颜色。
|
|
||||||
* **图2 (设计稿):** 纯印花设计稿。
|
|
||||||
* **功能:** 仅提供“图案元素”(线条、色号、比例)。
|
|
||||||
* **限制:** 提取图案时必须**完全去除**图2自带的背景色。
|
|
||||||
* **图3 (底图):** 平铺衣服底图。
|
|
||||||
* **功能:** 仅作为“皮肤”,提供衣服的**唯一基底颜色**与面料材质纹理。
|
|
||||||
* **限制:** 忽略其背景/场景,100%保留其衣服底色与质感。
|
|
||||||
|
|
||||||
## 2. 核心任务
|
|
||||||
构建一张最终合成的平铺图,必须同时满足:
|
|
||||||
1. **结构:** 保留图1的版型与褶皱细节。
|
|
||||||
2. **底色:** 强制应用图3的颜色与材质。
|
|
||||||
3. **图案:** 叠加图2的印花设计。
|
|
||||||
|
|
||||||
**最终效果 = 图1版型光影 + 图3底色材质 + 图2印花图案**
|
|
||||||
|
|
||||||
## 3. 执行规则 (严格执行,权重由高到低)
|
|
||||||
|
|
||||||
### 0. 禁止人物
|
|
||||||
* 严禁出现任何人体、模特、头颈、手臂或穿着效果。
|
|
||||||
* 必须保持纯平铺俯拍商品图形式。
|
|
||||||
|
|
||||||
### 1. 底色强制锁定 (Color Lock - 最高优先级)
|
|
||||||
* **基底替换:** 必须强制使用**图3的颜色**作为衣服的**唯一**基底色。
|
|
||||||
* **100%覆盖:** 彻底清除图1原有的面料颜色,严禁图1旧底色透出、混合或泛灰。
|
|
||||||
* **色调一致性:** 最终成衣的固有色必须与图3肉眼完全一致,不得因光影合成而发生色相偏移。
|
|
||||||
|
|
||||||
### 2. 纯印花剥离与叠加
|
|
||||||
* **去底处理:** 从图2中**仅提取**设计元素、线条、色块,**完全丢弃/移除**图2自身可能存在的任何背景色或画布底色。
|
|
||||||
* **叠加逻辑:** 将提取的纯图案,以“纹理融合”的方式叠加在图3的底色之上。印花必须依附于图3的底色,不可遮盖或改变图3的底色范围。
|
|
||||||
|
|
||||||
### 3. 印花尺寸适配
|
|
||||||
* 将提取的印花居中印在胸/背/衣身主体区域。
|
|
||||||
* 印花面积占比严格控制在衣身主体的30%-45%。
|
|
||||||
* 四周必须留白,严禁印花过大撑满衣服,或过小(<20%)。
|
|
||||||
|
|
||||||
### 4. 主体遮罩重建
|
|
||||||
* 精准识别图1的衣服本体完整区域(包含领口到下摆、袖子)。
|
|
||||||
* 用“图3底色+图2印花”合成出的新面料,完全覆盖图1的原有面料区域。
|
|
||||||
* 清除原区域的所有旧色和旧图案,只保留图1的边缘轮廓形态。
|
|
||||||
|
|
||||||
### 5. 精准形态贴合
|
|
||||||
* 合成后的面料必须严格遵循图1的平铺形态。
|
|
||||||
* 领口、袖口、下摆、侧缝等结构必须清晰。
|
|
||||||
* **自然变形:** 印花在遇到褶皱、翻折处时,必须随面料纹理发生自然的拉伸、挤压和变形,杜绝僵硬的“贴纸感”。
|
|
||||||
|
|
||||||
### 6. 光影重塑与色相锁定
|
|
||||||
* **借用光影:** 仅借用图1的光照方向、明暗对比度和投影位置。
|
|
||||||
* **色相锁定:** 在应用明暗变化时,只调整亮度和对比度,**严禁改变色相(Hue)**。
|
|
||||||
* 确保阴影处依然是图3颜色的深色版,高光处是图3颜色的亮色版,保持投影与图1完全一致。
|
|
||||||
|
|
||||||
### 7. 纯净输出
|
|
||||||
* 仅输出一张最终合成的平铺图。
|
|
||||||
* 背景环境、桌面、构图、拍摄角度必须与图1保持100%一致。
|
|
||||||
* 画面中不得出现人像、肢体、人台。
|
|
||||||
#留空=内置平铺三图提示词 FLAT_LAY_PROMPT
|
|
||||||
brief_index: 0 # 用第几个 safe 简报的提示词生成(0=第一个)
|
brief_index: 0 # 用第几个 safe 简报的提示词生成(0=第一个)
|
||||||
spu_code: "" # 款号(留空自动选第一个有本地底图的)
|
spu_code: "" # 款号(留空自动选第一个有本地底图的)
|
||||||
sku_code: "" # 颜色编码(留空自动选该款第一个有底图的);多个颜色用逗号分隔(如 "DG015-VT01,DG015-DARK HEATHER")
|
sku_code: "" # 颜色编码(留空自动选该款第一个有底图的);多个颜色用逗号分隔(如 "DG015-VT01,DG015-DARK HEATHER")
|
||||||
@@ -412,3 +367,19 @@ product:
|
|||||||
template_path: "templates/商品上传模版.xlsx" # 商品上传模板(可改为自由上传)
|
template_path: "templates/商品上传模版.xlsx" # 商品上传模板(可改为自由上传)
|
||||||
concurrency: 5 # Pinterest 模式 pipeline 总并发(设计生成→三合一→种草图→标题 同一管道串行,总并发=此值)
|
concurrency: 5 # Pinterest 模式 pipeline 总并发(设计生成→三合一→种草图→标题 同一管道串行,总并发=此值)
|
||||||
|
|
||||||
|
# —— 商品标题生成(多模态:分析服装图片 → SEO 标题)——
|
||||||
|
# 国家 → 提示词模板编号路由(TITLE_TEMPLATE_ROUTE);模板内容存 prompts/title_prompt_<编号>.md(可自由编辑)。
|
||||||
|
# 模板 1:英语市场(US/GB/AU/MX)→ en_title + cn_title
|
||||||
|
# 模板 2:日本市场(JP)→ en_title + cn_title + ja_title
|
||||||
|
# 模板 3:西班牙市场(ES)→ es_title + cn_title
|
||||||
|
# 读取优先级:运行根(exe 旁)prompts/ > 数据根 prompts/;文件缺失/留空回退代码内置默认。
|
||||||
|
# 新增国家/改路由只需在此配置;模板内可用 {year}/{month}/{season} 占位符(运行时自动替换为当前时间)。
|
||||||
|
title_templates:
|
||||||
|
route:
|
||||||
|
US: "1"
|
||||||
|
GB: "1"
|
||||||
|
JP: "2"
|
||||||
|
AU: "1"
|
||||||
|
MX: "1"
|
||||||
|
ES: "3"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# 童装短袖(男童/女童)特征库 —— 跨境电商童装场景图生成专用(场景完整约束版)
|
||||||
|
# 占位符:[模特特征] / [场景] / [服装风格] 随机取用
|
||||||
|
# 取用规则:
|
||||||
|
# 1. 商品类目(模版「类目」表头值)含「男童」→ 固定从 boy_kids 随机;含「女童」→ 固定从 girl_kids 随机。
|
||||||
|
# 2. 站点适配:北美/欧洲站优先「欧美系+多元族裔系」,东南亚站优先「东南亚裔」,日韩站优先「亚洲系」。
|
||||||
|
# 3. 场景特征每条即为完整独立提示词(含最高优先级/儿童保护/构图/负面/动作硬/稳定性六大约束块),
|
||||||
|
# 随机抽取后填入 [商品名称][材质][模特特征][服装风格] 占位符直接使用。
|
||||||
|
# 4. 同一商品全套图固定同一组 [模特特征]+[场景]+[服装风格],系列图调性统一。
|
||||||
|
# 描述规范:
|
||||||
|
# - 模特特征:年龄 + 族裔 + 发型发色 + 肤色 + 表情,仅描述长相,自然素颜无妆造,不含服装与下半身描述。
|
||||||
|
# - 服装风格:风格名 + 调性描述,均为童装短袖通用适龄风格。
|
||||||
|
# 场景内置约束体系:
|
||||||
|
# 儿童保护(素颜/适龄姿态/禁成人化与性感化)、完整着装(必穿下装+鞋子仅不入画/禁裸腿)、
|
||||||
|
# 构图(衣服100%入画/占比≥55%/头部≤1/3/印花为中心)、负面(禁改色/禁遮印花/结构部件保色/禁印花色延伸)、
|
||||||
|
# 全亮调光线,适配电商主图调性。
|
||||||
|
|
||||||
|
kids_features:
|
||||||
|
# ================= 模特特征(男童/女童) =================
|
||||||
|
model_features:
|
||||||
|
boy_kids:
|
||||||
|
# ---------- 亚洲系 ----------
|
||||||
|
- "5岁亚洲小男孩,黑色短发蘑菇头,白白嫩嫩,圆脸大眼睛,咧嘴笑露出小虎牙"
|
||||||
|
- "7岁亚洲小男孩,清爽黑色碎发,白净肌肤,元气满满的灿烂笑容"
|
||||||
|
- "9岁亚洲小男孩,利落黑色短发,健康肤色,阳光开朗自信的笑容"
|
||||||
|
# ---------- 欧美系 ----------
|
||||||
|
- "6岁欧美小男孩,金色蓬松卷发,白里透红,哈哈大笑的灿烂表情"
|
||||||
|
- "8岁欧美小男孩,浅棕短发,淡雀斑,麦色健康肌肤,无忧无虑的大笑"
|
||||||
|
# ---------- 多元族裔系 ----------
|
||||||
|
- "8岁非裔小男孩,利落寸头,健康深色肌肤,阳光开朗的大笑"
|
||||||
|
- "8岁拉丁裔小男孩,黑色微卷发,蜜色肌肤,热情明亮的笑容"
|
||||||
|
- "7岁东南亚裔小男孩,黑色短发,蜜糖色肌肤,清爽亲和的笑容"
|
||||||
|
|
||||||
|
girl_kids:
|
||||||
|
# ---------- 亚洲系 ----------
|
||||||
|
- "5岁亚洲小女孩,齐刘海黑色短发扎两个小揪揪,白白嫩嫩,呆萌可爱的笑容"
|
||||||
|
- "7岁亚洲小女孩,黑色长直发,白净肌肤,圆脸大眼睛,甜美乖巧的笑容"
|
||||||
|
- "9岁亚洲小女孩,黑长直发自然披肩,白皙皮肤,青春明朗的笑容"
|
||||||
|
# ---------- 欧美系 ----------
|
||||||
|
- "6岁欧美小女孩,金色卷发小公主头,白里透红,咯咯大笑的灿烂表情"
|
||||||
|
- "8岁欧美小女孩,金色马尾辫,淡淡雀斑,健康好气色,咧嘴大笑"
|
||||||
|
# ---------- 多元族裔系 ----------
|
||||||
|
- "7岁非裔小女孩,蓬松自然小卷发扎双马尾,健康深色肌肤,明亮的笑容"
|
||||||
|
- "8岁拉丁裔小女孩,深棕大波浪卷发,蜜色肌肤,热情爽朗的笑容"
|
||||||
|
- "8岁东南亚裔小女孩,黑直发齐刘海,蜜糖色肌肤,元气亲和的笑容"
|
||||||
|
|
||||||
|
# ================= 场景特征(完整约束版,男童/女童) =================
|
||||||
|
# ================= 场景特征(完整约束版,男童/女童) =================
|
||||||
|
scene_features:
|
||||||
|
boy_kids:
|
||||||
|
# ---------- 棚拍主图区 ----------
|
||||||
|
- name: "纯白背景正面站姿"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特下半身必须穿着基础色长裤或短裤并搭配鞋子,下装即使位于画面下沿之外也必须真实穿着。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止画面下沿出现裸腿,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童站姿自然挺拔,双手自然垂放于身体两侧或轻抓衣服下摆两侧,绝对不可抬手遮挡印花。【稳定性约束】儿童站在纯白无缝背景纸前,表情自然开心,整体呈现[服装风格]的调性,完整展示正面印花图案与[材质]面料的质感。采用电商标准高亮柔光,光线均匀无硬阴影,衣服受光均匀色彩还原准确。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
|
- name: "奶油黄彩底棚拍"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特下半身必须穿着基础色长裤或短裤并搭配鞋子,下装即使位于画面下沿之外也必须真实穿着。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童站姿自然,单手轻握一个纯色小皮球收在腰侧画面下沿,球体不遮挡衣服,绝对不可抬手遮挡印花。【稳定性约束】儿童站在低饱和奶油黄色无缝背景纸前,笑容灿烂自然,整体呈现[服装风格]的调性,突出印花的童趣感与[材质]面料的质感。采用高亮柔光箱均匀打光,背景与服装色彩柔和呼应不冲突,衣服受光均匀。画面比例为3:4。糖果色商业棚拍,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
|
# ---------- 户外玩耍区 ----------
|
||||||
|
- name: "公园游乐场站姿"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特穿着基础色短裤与运动鞋。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止游乐设施伸入前景遮挡衣服,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童站在阳光明媚的公园草坪的彩色滑梯旁,双手自然垂放或轻扶滑梯扶手位于画面下沿,笑容灿烂,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮。采用上午明亮的漫射自然光,衣服受光均匀色彩准确,背景为高度虚化的彩色滑梯与游乐设施,整体呈现[服装风格]的调性,完整展示正面印花图案与[材质]面料的质感,画面充满童趣活力。画面比例为3:4。清透童趣摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
|
- name: "海边沙滩站姿"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特穿着基础色短裤与凉鞋。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止沙子沾染弄脏衣服影响印花清晰度,禁止海水打湿变色衣服,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童站在细软的沙滩上,双手自然垂放于身体两侧,头微转向镜头笑容明朗,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮。采用正午强烈的顺光,衣服过曝得到良好控制色彩鲜明通透,背景为高度虚化的蔚蓝海水与远处海平线,整体呈现[服装风格]的调性,完整展示正面印花图案与[材质]面料的轻盈质感。画面比例为3:4。夏日清透色彩,高饱和度,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
|
# ---------- 居家生活区 ----------
|
||||||
|
- name: "儿童房积木桌玩耍"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特穿着基础色长裤。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止玩具与桌沿遮挡衣服主体,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童坐在明亮的儿童房积木桌旁,低头搭积木后抬头看向镜头露出笑容,双手举起的积木收在桌面高度的画面下沿不遮挡衣服,绝对不可将玩具举至胸口。【稳定性约束】背景必须保持干净明亮。采用明亮室内自然窗光与暖色灯带结合,衣服受光均匀,背景为高度虚化的温馨儿童房与彩色积木,整体呈现[服装风格]的调性,完整展示正面印花图案与[材质]面料的舒适质感。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
|
girl_kids:
|
||||||
|
# ---------- 棚拍主图区 ----------
|
||||||
|
- name: "纯白背景正面站姿"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特下半身必须穿着基础色长裤或短裤并搭配鞋子,下装即使位于画面下沿之外也必须真实穿着。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止画面下沿出现裸腿,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童站姿自然挺拔,双手自然垂放于身体两侧或轻抓衣服下摆两侧,绝对不可抬手遮挡印花。【稳定性约束】儿童站在纯白无缝背景纸前,表情自然甜美,整体呈现[服装风格]的调性,完整展示正面印花图案与[材质]面料的质感。采用电商标准高亮柔光,光线均匀无硬阴影,衣服受光均匀色彩还原准确。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
|
- name: "奶油粉彩底棚拍"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特下半身必须穿着基础色长裤或短裤并搭配鞋子,下装即使位于画面下沿之外也必须真实穿着。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童站姿自然,单手轻轻抱一只浅色小毛绒玩具收在腰腹高度的画面下沿,玩具不遮挡衣服,绝对不可抬手遮挡印花。【稳定性约束】儿童站在低饱和奶油粉色无缝背景纸前,笑容甜美自然,整体呈现[服装风格]的调性,突出印花的童趣感与[材质]面料的质感。采用高亮柔光箱均匀打光,背景与服装色彩柔和呼应不冲突,衣服受光均匀。画面比例为3:4。糖果色商业棚拍,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
|
# ---------- 户外玩耍区 ----------
|
||||||
|
- name: "公园草坪花朵场景"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特穿着基础色短裤与凉鞋。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止花朵枝叶伸入前景遮挡衣服,禁止头发垂落遮挡印花区域,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童站在阳光明媚的公园草坪上,身旁有秋千虚化背景,双手自然垂放,头微歪露出甜美的笑容,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮。采用上午明亮的漫射自然光,衣服受光均匀色彩准确,背景为大面积虚化的绿色草坪与五彩花朵光斑,整体呈现[服装风格]的调性,完整展示正面印花图案与[材质]面料的轻盈质感,画面梦幻通透。画面比例为3:4。清透氧气感色彩,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。
|
||||||
|
|
||||||
|
- name: "泡泡机欢乐场景"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特穿着基础色下装与鞋子。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止泡泡大面积覆盖衣服表面影响印花清晰度,泡泡只点缀衣服周围的空气区域,禁止头发甩动遮挡印花区域,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童在户外追逐飘浮的肥皂泡,双手自然张开于身体两侧小幅挥舞,表情开心大笑,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮。采用下午明亮的自然光,衣服受光均匀,背景为高度虚化的公园草坪与漂浮的七彩泡泡,整体呈现[服装风格]的调性,完整展示正面印花图案与[材质]面料的轻盈透气质感,画面梦幻通透。画面比例为3:4。清透童趣摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
|
# ---------- 居家生活区 ----------
|
||||||
|
- name: "飘窗边阅读场景"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中童装的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其穿在一位[模特特征]的身上,模特仅穿这一件上衣,绝对禁止在外面叠加任何外套或夹克;同时模特穿着基础色长裤。【儿童保护约束】模特为一名健康的儿童模特,呈现日常自然的儿童形象,仅用于服装展示;自然素颜状态,禁止任何成人化妆造;姿态符合儿童天性,禁止成人化姿势与性感化表达。【构图约束】取景自头顶上方少许留白至衣服下摆下方一掌宽处,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;童装占比不低于55%,头部占比不超过画面高度的三分之一,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止绘本遮挡衣服正面与印花,绘本只放在儿童腿上腰腹高度的画面下沿,禁止改变抽绳、帽绳、罗纹等服装结构部件的原始颜色,禁止将印花颜色延伸绘制到结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】儿童坐在明亮的飘窗边看绘本,抬头看向镜头露出甜美的笑容,双手扶绘本收在大腿高度的画面下沿,绝对不可将绘本举至胸口。【稳定性约束】背景必须保持干净明亮。室内采用柔和的窗光侧顺光保证衣服受光均匀,背景为高度虚化的温馨飘窗与窗纱光晕,整体呈现[服装风格]的调性,完整展示正面印花图案与[材质]面料的柔软亲肤质感。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
|
# ================= 服装风格(男童/女童) =================
|
||||||
|
style_features:
|
||||||
|
boy_kids:
|
||||||
|
- "童趣休闲风,活泼明快、日常亲和,适合小动物与恐龙印花短袖"
|
||||||
|
- "运动活力风,阳光朝气、健康开朗,适合运动号码与徽章印花短袖"
|
||||||
|
- "美式街头童趣风,小小酷盖、张扬有度,适合大字母与卡车图案短袖"
|
||||||
|
- "加州度假风,阳光灿烂、自由玩耍,适合热带元素与彩色字母短袖"
|
||||||
|
- "简约基础风,干净纯粹、百搭耐穿,适合纯色与小logo短袖"
|
||||||
|
|
||||||
|
girl_kids:
|
||||||
|
- "童趣甜美风,奶呼呼的可爱、柔和治愈,适合小动物与卡通图案短袖"
|
||||||
|
- "元气活泼风,明亮干净、元气满满,适合水果与星星图案短袖"
|
||||||
|
- "热带水果乐园风,色彩缤纷、甜蜜多汁,适合西瓜菠萝印花短袖"
|
||||||
|
- "清新度假风,阳光灿烂、夏日沙滩感,适合海洋元素与彩色字母短袖"
|
||||||
|
- "奶油温柔风,温暖治愈、软糯高级,适合纯色与小刺绣短袖"
|
||||||
+3
-1
@@ -49,7 +49,9 @@ CREATE TABLE SPU (
|
|||||||
belt TEXT DEFAULT '否',
|
belt TEXT DEFAULT '否',
|
||||||
occasion TEXT DEFAULT '休闲外出',
|
occasion TEXT DEFAULT '休闲外出',
|
||||||
scene TEXT DEFAULT '休闲',
|
scene TEXT DEFAULT '休闲',
|
||||||
hemline_shape TEXT DEFAULT '伞状'
|
hemline_shape TEXT DEFAULT '伞状',
|
||||||
|
kids_type TEXT,
|
||||||
|
kids_age TEXT
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -13,6 +13,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import yaml
|
||||||
|
|
||||||
# 模型调用一律直连:用户常开 VPN(系统代理),LLM 网关多为国内/自建,走代理会被拦截或变慢。
|
# 模型调用一律直连:用户常开 VPN(系统代理),LLM 网关多为国内/自建,走代理会被拦截或变慢。
|
||||||
# 仅请求级 proxies=NO_PROXY 直连,不设置进程级 NO_PROXY 环境变量(避免影响 Google Trends 等外部采集)。
|
# 仅请求级 proxies=NO_PROXY 直连,不设置进程级 NO_PROXY 环境变量(避免影响 Google Trends 等外部采集)。
|
||||||
@@ -101,12 +102,14 @@ Rules:
|
|||||||
- Return ONLY JSON of shape: {"style_seeds": [...], "related_seeds": [...]}'''
|
- Return ONLY JSON of shape: {"style_seeds": [...], "related_seeds": [...]}'''
|
||||||
|
|
||||||
|
|
||||||
# —— 商品标题生成(多模态:分析服装图片 → 中英双语 SEO 标题)——
|
# —— 商品标题生成(多模态:分析服装图片 → SEO 标题)——
|
||||||
# 模板字典按编号存放;TITLE_TEMPLATE_ROUTE 按国家路由到模板编号。
|
# 提示词模板存 prompts/title_prompt_<编号>.md(可自由编辑,优先运行根 exe 旁,回退数据根);
|
||||||
|
# 国家 → 模板编号路由存 config.yaml 顶层 title_templates.route(可自由编辑)。
|
||||||
# 模板 1:英语市场(US/GB/AU/MX)→ en_title + cn_title
|
# 模板 1:英语市场(US/GB/AU/MX)→ en_title + cn_title
|
||||||
# 模板 2:日本市场(JP)→ en_title + cn_title + ja_title
|
# 模板 2:日本市场(JP)→ en_title + cn_title + ja_title
|
||||||
# 模板 3:西班牙市场(ES)→ es_title + cn_title
|
# 模板 3:西班牙市场(ES)→ es_title + cn_title
|
||||||
TITLE_TEMPLATES: Dict[str, str] = {
|
# 以下为「内置默认」兜底:md/yaml 缺失或留空时回退,避免标题生成中断。
|
||||||
|
_TITLE_TEMPLATES_FALLBACK: Dict[str, str] = {
|
||||||
"1": '''# Role
|
"1": '''# Role
|
||||||
你是一位资深的跨境服装运营专家,精通英语电商的SEO标题逻辑。你的任务是通过分析服装图片,生成高权重的英语-中文商品标题。
|
你是一位资深的跨境服装运营专家,精通英语电商的SEO标题逻辑。你的任务是通过分析服装图片,生成高权重的英语-中文商品标题。
|
||||||
|
|
||||||
@@ -172,8 +175,8 @@ TITLE_TEMPLATES: Dict[str, str] = {
|
|||||||
{"es_title": "Título en español", "cn_title": "中文标题"}''',
|
{"es_title": "Título en español", "cn_title": "中文标题"}''',
|
||||||
}
|
}
|
||||||
|
|
||||||
# 国家 → 标题模板编号(JP 路由到模板 2,ES 路由到模板 3,其余默认模板 1;后续可按国家新增模板)
|
# 内置默认路由(config.yaml title_templates.route 缺失/留空时回退;JP→模板2,ES→模板3,其余默认模板1)
|
||||||
TITLE_TEMPLATE_ROUTE: Dict[str, str] = {
|
_TITLE_ROUTE_FALLBACK: Dict[str, str] = {
|
||||||
"US": "1",
|
"US": "1",
|
||||||
"GB": "1",
|
"GB": "1",
|
||||||
"JP": "2",
|
"JP": "2",
|
||||||
@@ -183,6 +186,33 @@ TITLE_TEMPLATE_ROUTE: Dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_title_prompt(tpl_no: str) -> str:
|
||||||
|
"""读取 prompts/title_prompt_<编号>.md:优先运行根(exe 旁,可编辑),回退数据根;找不到/空返回空串。"""
|
||||||
|
for base in (runtime_root(), project_root()):
|
||||||
|
p = base / "prompts" / f"title_prompt_{tpl_no}.md"
|
||||||
|
if p.exists():
|
||||||
|
text = p.read_text(encoding="utf-8").strip()
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _load_title_route() -> Dict[str, str]:
|
||||||
|
"""从 config.yaml 顶层 title_templates.route 读取国家→模板编号路由;缺失/留空回退内置默认。"""
|
||||||
|
for base in (runtime_root(), project_root()):
|
||||||
|
p = base / "config.yaml"
|
||||||
|
if not p.exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
||||||
|
route = (data.get("title_templates") or {}).get("route") or {}
|
||||||
|
if isinstance(route, dict) and route:
|
||||||
|
return {str(k): str(v) for k, v in route.items()}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[titles] 读取 config.yaml title_templates.route 失败: {e}")
|
||||||
|
return dict(_TITLE_ROUTE_FALLBACK)
|
||||||
|
|
||||||
|
|
||||||
def _inject_now(prompt: str) -> str:
|
def _inject_now(prompt: str) -> str:
|
||||||
"""把模板中的 {year}/{month}/{season} 替换为当前时间(用 replace 避免 JSON 花括号冲突)。"""
|
"""把模板中的 {year}/{month}/{season} 替换为当前时间(用 replace 避免 JSON 花括号冲突)。"""
|
||||||
import datetime
|
import datetime
|
||||||
@@ -197,8 +227,10 @@ def _inject_now(prompt: str) -> str:
|
|||||||
|
|
||||||
def resolve_title_prompt(country: str = "") -> str:
|
def resolve_title_prompt(country: str = "") -> str:
|
||||||
"""按国家解析标题生成提示词(自动注入当前时间变量);未知国家/留空回退模板 1。"""
|
"""按国家解析标题生成提示词(自动注入当前时间变量);未知国家/留空回退模板 1。"""
|
||||||
tpl_no = TITLE_TEMPLATE_ROUTE.get(country or "", "1")
|
route = _load_title_route()
|
||||||
return _inject_now(TITLE_TEMPLATES.get(tpl_no, TITLE_TEMPLATES["1"]))
|
tpl_no = route.get(country or "", "1")
|
||||||
|
prompt = _read_title_prompt(tpl_no) or _TITLE_TEMPLATES_FALLBACK.get(tpl_no, _TITLE_TEMPLATES_FALLBACK["1"])
|
||||||
|
return _inject_now(prompt)
|
||||||
|
|
||||||
|
|
||||||
def build_seed_user_prompt(context: Dict[str, Any]) -> str:
|
def build_seed_user_prompt(context: Dict[str, Any]) -> str:
|
||||||
@@ -810,7 +842,8 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
def generate_title(self, image_path: str, system_prompt: str = "", country: str = "") -> Dict[str, Any]:
|
def generate_title(self, image_path: str, system_prompt: str = "", country: str = "") -> Dict[str, Any]:
|
||||||
"""多模态:分析服装图片,生成商品标题(按国家路由模板)。
|
"""多模态:分析服装图片,生成商品标题(按国家路由模板)。
|
||||||
|
|
||||||
系统提示词:显式传入优先;否则按 country 经 TITLE_TEMPLATE_ROUTE 路由到对应模板。
|
系统提示词:显式传入优先;否则按 country 经 config.yaml title_templates.route 路由到
|
||||||
|
prompts/title_prompt_<编号>.md 对应模板(缺失回退内置默认)。
|
||||||
模板 1(US/GB/AU/MX)返回 {"en_title","cn_title"};
|
模板 1(US/GB/AU/MX)返回 {"en_title","cn_title"};
|
||||||
模板 2(JP)额外返回 {"ja_title"};
|
模板 2(JP)额外返回 {"ja_title"};
|
||||||
模板 3(ES)返回 {"es_title","cn_title"}。
|
模板 3(ES)返回 {"es_title","cn_title"}。
|
||||||
|
|||||||
+21
-16
@@ -113,18 +113,26 @@ FLAT_LAY_PROMPT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _active_prompt(kind: str, prompts: Optional[Dict] = None) -> str:
|
def _read_prompt_md(filename: str) -> str:
|
||||||
|
"""读取 prompts/<filename> 提示词文件:优先运行根(exe 旁,可编辑),回退数据根;找不到/空返回空串。"""
|
||||||
|
for base in (runtime_root(), project_root()):
|
||||||
|
p = base / "prompts" / filename
|
||||||
|
if p.exists():
|
||||||
|
text = p.read_text(encoding="utf-8").strip()
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _active_prompt(kind: str) -> str:
|
||||||
"""返回当前图源应使用的合成提示词。
|
"""返回当前图源应使用的合成提示词。
|
||||||
|
|
||||||
kind="model" → 模特三图提示词(配置覆盖优先生效,否则内置 MODEL_WEAR_PROMPT);
|
kind="model" → 模特三图提示词(优先读 prompts/model_prompt.md,缺失/留空回退内置 MODEL_WEAR_PROMPT);
|
||||||
kind="flat" → 平铺三图提示词(配置覆盖优先生效,否则内置 FLAT_LAY_PROMPT)。
|
kind="flat" → 平铺三图提示词(优先读 prompts/flat_prompt.md,缺失/留空回退内置 FLAT_LAY_PROMPT)。
|
||||||
"""
|
"""
|
||||||
cfgs = prompts or {}
|
|
||||||
if kind == "flat":
|
if kind == "flat":
|
||||||
return (str(cfgs.get("flat_prompt") or "").strip()
|
return _read_prompt_md("flat_prompt.md") or FLAT_LAY_PROMPT
|
||||||
or FLAT_LAY_PROMPT)
|
return _read_prompt_md("model_prompt.md") or MODEL_WEAR_PROMPT
|
||||||
return (str(cfgs.get("model_prompt") or "").strip()
|
|
||||||
or MODEL_WEAR_PROMPT)
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_sku(db_path, basemap_root, spu_code: str, sku_code: str, colors=None) -> Optional[str]:
|
def _resolve_sku(db_path, basemap_root, spu_code: str, sku_code: str, colors=None) -> Optional[str]:
|
||||||
@@ -187,7 +195,7 @@ def _process_spu(
|
|||||||
db_path, basemap_root, material_root, category, prod_dir, brief, ib,
|
db_path, basemap_root, material_root, category, prod_dir, brief, ib,
|
||||||
spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="",
|
spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="",
|
||||||
img_code="", model_img=None, design_size="1024x1024", compose_size="1536x2048",
|
img_code="", model_img=None, design_size="1024x1024", compose_size="1536x2048",
|
||||||
on_503=None, model_kind="model", prompts=None,
|
on_503=None, model_kind="model",
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
|
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
|
||||||
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
|
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
|
||||||
@@ -195,7 +203,6 @@ def _process_spu(
|
|||||||
(按货号命名,包含该货号对应的所有图片)。
|
(按货号命名,包含该货号对应的所有图片)。
|
||||||
on_503: 致命图像服务错误(503/账户不可用)回调(供调用方提前终止任务)。
|
on_503: 致命图像服务错误(503/账户不可用)回调(供调用方提前终止任务)。
|
||||||
model_kind: 图源类型 "model"(模特图)/ "flat"(平铺图),决定用哪个合成提示词。
|
model_kind: 图源类型 "model"(模特图)/ "flat"(平铺图),决定用哪个合成提示词。
|
||||||
prompts: {"model": str, "flat": str} 可配置提示词覆盖(来自 config.product.mark_dirs);缺省用内置。
|
|
||||||
返回 result dict;内部异常已兜底,不中断。
|
返回 result dict;内部异常已兜底,不中断。
|
||||||
"""
|
"""
|
||||||
# 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里
|
# 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里
|
||||||
@@ -324,8 +331,8 @@ def _process_spu(
|
|||||||
elif model_img is not None:
|
elif model_img is not None:
|
||||||
composite_path = str(prod_dir / f"{img_code}_composite.png")
|
composite_path = str(prod_dir / f"{img_code}_composite.png")
|
||||||
try:
|
try:
|
||||||
# 三图合成:按图源类型选提示词(可配置覆盖优先,否则内置 MODEL_WEAR/FLAT_LAY)
|
# 三图合成:按图源类型选提示词(优先读 prompts/*.md,否则内置 MODEL_WEAR/FLAT_LAY)
|
||||||
wear_prompt = _active_prompt(model_kind, prompts)
|
wear_prompt = _active_prompt(model_kind)
|
||||||
kind_label = "平铺" if model_kind == "flat" else "模特"
|
kind_label = "平铺" if model_kind == "flat" else "模特"
|
||||||
print(f"{tag} {kind_label}三图合成提交中(3 参考图 img2img,网关处理约 2-6 分钟,请耐心等待)…")
|
print(f"{tag} {kind_label}三图合成提交中(3 参考图 img2img,网关处理约 2-6 分钟,请耐心等待)…")
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
@@ -407,7 +414,7 @@ def _process_spu(
|
|||||||
continue
|
continue
|
||||||
cp = str(prod_dir / f"{img_code}_{str(sc).split('-')[-1]}_composite.png")
|
cp = str(prod_dir / f"{img_code}_{str(sc).split('-')[-1]}_composite.png")
|
||||||
try:
|
try:
|
||||||
ib.print(_active_prompt(model_kind, prompts), str(model_img), cp,
|
ib.print(_active_prompt(model_kind), str(model_img), cp,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
extra_images=[design_path, str(bm)], # 图2印花, 图3该色底图
|
extra_images=[design_path, str(bm)], # 图2印花, 图3该色底图
|
||||||
size=compose_size) # 合成图尺寸按 config compose.size
|
size=compose_size) # 合成图尺寸按 config compose.size
|
||||||
@@ -546,7 +553,6 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
# → product_node 用对应提示词合成(模特三图 / 平铺三图)
|
# → product_node 用对应提示词合成(模特三图 / 平铺三图)
|
||||||
model_assign: Dict[int, Any] = {}
|
model_assign: Dict[int, Any] = {}
|
||||||
mark_dirs = pcfg.get("mark_dirs") or {}
|
mark_dirs = pcfg.get("mark_dirs") or {}
|
||||||
prompts_cfg = (mark_dirs.get("1") or {})
|
|
||||||
try:
|
try:
|
||||||
from graph.product import build_mark_sources
|
from graph.product import build_mark_sources
|
||||||
sources = build_mark_sources(material_root, mark_dirs, category)
|
sources = build_mark_sources(material_root, mark_dirs, category)
|
||||||
@@ -559,7 +565,7 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if source_pool:
|
if source_pool:
|
||||||
for _i in range(len(worklist)):
|
for _i in range(len(worklist)):
|
||||||
img, kind = random.choice(source_pool) # 每任务随机抽一张(含 kind)
|
img, kind = random.choice(source_pool) # 每任务随机抽一张(含 kind)
|
||||||
model_assign[_i] = {"img": img, "kind": kind, "prompts": prompts_cfg}
|
model_assign[_i] = {"img": img, "kind": kind}
|
||||||
print(f"[product] 任务级图源分配:{len(model_assign)} 个产品任务(mark_dirs 模特/平铺图随机抽,含 kind)")
|
print(f"[product] 任务级图源分配:{len(model_assign)} 个产品任务(mark_dirs 模特/平铺图随机抽,含 kind)")
|
||||||
else:
|
else:
|
||||||
print("[product] material_library 无任何可用图(模特/平铺均无)→ 跳过合成,仅导出模板")
|
print("[product] material_library 无任何可用图(模特/平铺均无)→ 跳过合成,仅导出模板")
|
||||||
@@ -628,7 +634,6 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
country, img_code=img_code,
|
country, img_code=img_code,
|
||||||
model_img=_src.get("img"), # 按任务序号取独立随机图源
|
model_img=_src.get("img"), # 按任务序号取独立随机图源
|
||||||
model_kind=_src.get("kind", "model"),
|
model_kind=_src.get("kind", "model"),
|
||||||
prompts=_src.get("prompts"),
|
|
||||||
design_size=str((config.get("compose") or {}).get("design_size") or "1024x1024"),
|
design_size=str((config.get("compose") or {}).get("design_size") or "1024x1024"),
|
||||||
compose_size=str((config.get("compose") or {}).get("size") or "1536x2048"))
|
compose_size=str((config.get("compose") or {}).get("size") or "1536x2048"))
|
||||||
if r:
|
if r:
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
category = read_template_category(tp)
|
category = read_template_category(tp)
|
||||||
gender = gender_from_category(category)
|
gender = gender_from_category(category)
|
||||||
if gender:
|
if gender:
|
||||||
print(f"[seed_shot] 类目「{category[:30]}…」含{'男' if gender == 'male' else '女'} → 固定 {gender} 模特")
|
label = {"male": "男", "female": "女", "boy_kids": "男童", "girl_kids": "女童"}.get(gender, gender)
|
||||||
|
print(f"[seed_shot] 类目「{category[:30]}…」检测到 {label} → 固定 {gender} 模特")
|
||||||
elif category:
|
elif category:
|
||||||
print(f"[seed_shot] 类目「{category[:30]}…」无男/女 → 男女模特随机")
|
print(f"[seed_shot] 类目「{category[:30]}…」无男/女 → 男女模特随机")
|
||||||
|
|
||||||
@@ -141,7 +142,8 @@ def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
continue
|
continue
|
||||||
generated = generate_seed_shots(ib, base, cn, material, n, str(shot_dir),
|
generated = generate_seed_shots(ib, base, cn, material, n, str(shot_dir),
|
||||||
r.get("composite_negative", ""),
|
r.get("composite_negative", ""),
|
||||||
size=size, prefix=pfx, gender=gender)
|
size=size, prefix=pfx, gender=gender,
|
||||||
|
retries=int(ss_cfg.get("retries", 3)))
|
||||||
paths.extend(generated)
|
paths.extend(generated)
|
||||||
if not paths:
|
if not paths:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -29,6 +29,38 @@ def _template_out_path(prod_dir: Path, tpl_name: str) -> Path:
|
|||||||
return prod_dir / f"{tpl_name}_已填写_{int(time.time())}.xlsx"
|
return prod_dir / f"{tpl_name}_已填写_{int(time.time())}.xlsx"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_preview(output_dir: Path, products: List[Dict[str, Any]]) -> None:
|
||||||
|
"""生成 Preview 文件夹:仅成功导入模板的产品(template_path 已设置),
|
||||||
|
复制其 _composite.oss.jpg(压缩版合成图)到 Preview,并生成 result.xlsx
|
||||||
|
(货号 | 中文标题 | 英文标题 三列)。"""
|
||||||
|
import shutil
|
||||||
|
from openpyxl import Workbook
|
||||||
|
preview_dir = output_dir / "Preview"
|
||||||
|
preview_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
rows: List[tuple] = []
|
||||||
|
for r in products:
|
||||||
|
if not r.get("template_path"):
|
||||||
|
continue
|
||||||
|
code = str(r.get("img_code") or r.get("oss_code") or "")
|
||||||
|
comp = r.get("composite_path") or r.get("printed_path")
|
||||||
|
if comp:
|
||||||
|
oss_file = Path(comp).with_suffix(".oss.jpg")
|
||||||
|
if oss_file.exists():
|
||||||
|
dst = preview_dir / oss_file.name
|
||||||
|
if not dst.exists():
|
||||||
|
shutil.copy2(oss_file, dst)
|
||||||
|
rows.append((code, r.get("cn_title") or "", r.get("en_title") or ""))
|
||||||
|
if rows:
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "result"
|
||||||
|
ws.append(["货号", "中文标题", "英文标题"])
|
||||||
|
for row in rows:
|
||||||
|
ws.append(list(row))
|
||||||
|
wb.save(str(preview_dir / "result.xlsx"))
|
||||||
|
print(f"[template] Preview 已生成({len(rows)} 个成功产品): {preview_dir}")
|
||||||
|
|
||||||
|
|
||||||
@with_fallback("template_export")
|
@with_fallback("template_export")
|
||||||
def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
products: List[Dict[str, Any]] = state.get("product") or []
|
products: List[Dict[str, Any]] = state.get("product") or []
|
||||||
@@ -68,7 +100,8 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
break
|
break
|
||||||
|
|
||||||
from graph.template_export import (export_products, _resolve_component_map,
|
from graph.template_export import (export_products, _resolve_component_map,
|
||||||
_resolve_season_map, _resolve_pattern_map)
|
_resolve_season_map, _resolve_pattern_map,
|
||||||
|
_resolve_target_audience_map, _resolve_kids_type_map)
|
||||||
tdir = (pcfg.get("template_dir") or "").strip() or str(Path(tp).parent)
|
tdir = (pcfg.get("template_dir") or "").strip() or str(Path(tp).parent)
|
||||||
prod_dir = output_dir / "product"
|
prod_dir = output_dir / "product"
|
||||||
prod_dir.mkdir(parents=True, exist_ok=True)
|
prod_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -124,12 +157,15 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
component_map=_resolve_component_map(config),
|
component_map=_resolve_component_map(config),
|
||||||
season_map=_resolve_season_map(config),
|
season_map=_resolve_season_map(config),
|
||||||
pattern_map=_resolve_pattern_map(config),
|
pattern_map=_resolve_pattern_map(config),
|
||||||
|
target_audience_map=_resolve_target_audience_map(config),
|
||||||
|
kids_type_map=_resolve_kids_type_map(config),
|
||||||
)
|
)
|
||||||
for r in products:
|
for r in products:
|
||||||
if (r.get("composite_path") or r.get("printed_path")) and (r.get("en_title") or "").strip():
|
if (r.get("composite_path") or r.get("printed_path")) and (r.get("en_title") or "").strip():
|
||||||
r["template_path"] = str(out)
|
r["template_path"] = str(out)
|
||||||
exported.append(str(out))
|
exported.append(str(out))
|
||||||
print(f"[template] 商品上传模板已生成({len(batch)} 个产品一次合并): {out}")
|
print(f"[template] 商品上传模板已生成({len(batch)} 个产品一次合并): {out}")
|
||||||
|
_build_preview(output_dir, products)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
errors.append({"node": "template_export", "type": type(e).__name__,
|
errors.append({"node": "template_export", "type": type(e).__name__,
|
||||||
"message": f"模板批量导出失败: {e}", "trace": ""})
|
"message": f"模板批量导出失败: {e}", "trace": ""})
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ class PinterestPipeline:
|
|||||||
category = read_template_category(tp)
|
category = read_template_category(tp)
|
||||||
self._gender = gender_from_category(category)
|
self._gender = gender_from_category(category)
|
||||||
if self._gender:
|
if self._gender:
|
||||||
print(f"[pinterest_pipeline] 类目「{category[:30]}…」含{'男' if self._gender == 'male' else '女'} → 固定 {self._gender} 模特")
|
label = {"male": "男", "female": "女", "boy_kids": "男童", "girl_kids": "女童"}.get(self._gender, self._gender)
|
||||||
|
print(f"[pinterest_pipeline] 类目「{category[:30]}…」检测到 {label} → 固定 {self._gender} 模特")
|
||||||
elif category:
|
elif category:
|
||||||
print(f"[pinterest_pipeline] 类目「{category[:30]}…」无男/女 → 男女模特随机")
|
print(f"[pinterest_pipeline] 类目「{category[:30]}…」无男/女 → 男女模特随机")
|
||||||
|
|
||||||
@@ -240,8 +241,7 @@ class PinterestPipeline:
|
|||||||
if not pool:
|
if not pool:
|
||||||
continue
|
continue
|
||||||
img, kind = _random.choice(pool) # 每任务独立随机抽一张(含 kind)
|
img, kind = _random.choice(pool) # 每任务独立随机抽一张(含 kind)
|
||||||
assign[key] = {"img": img, "kind": kind,
|
assign[key] = {"img": img, "kind": kind}
|
||||||
"prompts": (mark_dirs.get(mark) or mark_dirs.get("1") or {})}
|
|
||||||
return assign
|
return assign
|
||||||
|
|
||||||
def _load_materials(self) -> Dict[str, str]:
|
def _load_materials(self) -> Dict[str, str]:
|
||||||
@@ -674,8 +674,7 @@ class PinterestPipeline:
|
|||||||
if spu is not None and int(spu.get("mark") or 0) != 1:
|
if spu is not None and int(spu.get("mark") or 0) != 1:
|
||||||
return {"model_img": img} # 非 mark=1 走旧逻辑(不传 kind,product_node 按其 mark 自行判定)
|
return {"model_img": img} # 非 mark=1 走旧逻辑(不传 kind,product_node 按其 mark 自行判定)
|
||||||
return {"model_img": img,
|
return {"model_img": img,
|
||||||
"model_kind": src.get("kind", "model"),
|
"model_kind": src.get("kind", "model")}
|
||||||
"prompts": src.get("prompts") or {}}
|
|
||||||
|
|
||||||
def _process_spu(self, brief: Dict[str, Any], spu, skus: str, img_code: str,
|
def _process_spu(self, brief: Dict[str, Any], spu, skus: str, img_code: str,
|
||||||
design_path: str, task_idx: int = 0) -> Optional[Dict[str, Any]]:
|
design_path: str, task_idx: int = 0) -> Optional[Dict[str, Any]]:
|
||||||
@@ -769,7 +768,8 @@ class PinterestPipeline:
|
|||||||
try:
|
try:
|
||||||
generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir),
|
generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir),
|
||||||
r.get("composite_negative", ""),
|
r.get("composite_negative", ""),
|
||||||
size=size, prefix=pfx, gender=self._gender)
|
size=size, prefix=pfx, gender=self._gender,
|
||||||
|
retries=int(ss_cfg.get("retries", 3)))
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
if self.is_fatal_503(e):
|
if self.is_fatal_503(e):
|
||||||
self.record_503()
|
self.record_503()
|
||||||
|
|||||||
+112
-16
@@ -44,9 +44,47 @@ def load_templates() -> List[Dict[str, str]]:
|
|||||||
for t in tpls if t.get("prompt")]
|
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]:
|
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")
|
data = _load_yaml("configs/model_features.yaml")
|
||||||
mf = data.get("model_features") or []
|
mf = data.get("model_features") or []
|
||||||
feats: List[str] = []
|
feats: List[str] = []
|
||||||
@@ -62,6 +100,16 @@ def load_model_features(gender: Optional[str] = None) -> List[str]:
|
|||||||
return feats
|
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:
|
def read_template_category(template_path: str) -> str:
|
||||||
"""读取模版「类目」表头对应的值(如 服装、鞋靴和珠宝饰品>男士时尚>男装>男装上衣、T恤、衬衫>男装T恤)。
|
"""读取模版「类目」表头对应的值(如 服装、鞋靴和珠宝饰品>男士时尚>男装>男装上衣、T恤、衬衫>男装T恤)。
|
||||||
遍历所有 sheet(类目表头可能在「模版」等 sheet),找到即返回其下一行同列值。"""
|
遍历所有 sheet(类目表头可能在「模版」等 sheet),找到即返回其下一行同列值。"""
|
||||||
@@ -85,7 +133,13 @@ def read_template_category(template_path: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def gender_from_category(category: str) -> Optional[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:
|
if "男" in category:
|
||||||
return "male"
|
return "male"
|
||||||
if "女" in category:
|
if "女" in category:
|
||||||
@@ -93,8 +147,15 @@ def gender_from_category(category: str) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_style_features() -> List[str]:
|
def load_style_features(gender: Optional[str] = None) -> List[str]:
|
||||||
"""服装风格列表(style_features.yaml,随机取一条替换 [服装风格];无配置时内置兜底)。"""
|
"""服装风格列表(童装从 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")
|
data = _load_yaml("configs/style_features.yaml")
|
||||||
feats = [str(f) for f in (data.get("style_features") or []) if str(f).strip()]
|
feats = [str(f) for f in (data.get("style_features") or []) if str(f).strip()]
|
||||||
if not feats:
|
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,
|
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 = template_prompt.replace("[商品名称]", (cn_title or "").strip() or "这件衣服")
|
||||||
out = out.replace("[材质]", (material or "").strip() or "面料")
|
out = out.replace("[材质]", (material or "").strip() or "面料")
|
||||||
out = out.replace("[模特特征]", (model_feature or "").strip() or "模特")
|
out = out.replace("[模特特征]", (model_feature or "").strip() or "模特")
|
||||||
out = out.replace("[服装风格]", (style_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
|
return out
|
||||||
|
|
||||||
|
|
||||||
def generate_seed_shots(image_backend, base_image: str, cn_title: str, material: str,
|
def generate_seed_shots(image_backend, base_image: str, cn_title: str, material: str,
|
||||||
count: int, out_dir: str, negative: str = "",
|
count: int, out_dir: str, negative: str = "",
|
||||||
size: str = "1536x2048", prefix: str = "",
|
size: str = "1536x2048", prefix: str = "",
|
||||||
gender: Optional[str] = None) -> List[str]:
|
gender: Optional[str] = None, retries: int = 3) -> List[str]:
|
||||||
"""生成 count 张种草图(img2img,图1=合成图)。返回产物路径列表。
|
"""生成 count 张种草图(img2img,图1=合成图)。返回产物路径列表。
|
||||||
size: 种草图统一 1536x2048(与合成图一致)。
|
size: 种草图统一 1536x2048(与合成图一致)。
|
||||||
prefix: 货号前缀(对应产品货号,命名 {prefix}_{随机4位}.png,不覆盖旧文件)。
|
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()
|
templates = load_templates()
|
||||||
features = load_model_features(gender=gender)
|
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 = Path(out_dir)
|
||||||
out.mkdir(parents=True, exist_ok=True)
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
paths: List[str] = []
|
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):
|
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位避免自增序号/覆盖)
|
# 命名:{货号}_{随机4位}.png(按货号命名,随机4位避免自增序号/覆盖)
|
||||||
while True:
|
while True:
|
||||||
rand4 = f"{random.randint(0, 9999):04d}"
|
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")
|
else out / f"seed_{rand4}.png")
|
||||||
if not Path(out_path).exists():
|
if not Path(out_path).exists():
|
||||||
break
|
break
|
||||||
|
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:
|
try:
|
||||||
image_backend.print(prompt, base_image, out_path, negative, size=size)
|
image_backend.print(prompt, base_image, out_path, negative, size=size)
|
||||||
paths.append(out_path)
|
paths.append(out_path)
|
||||||
print(f"[seed_shot] 已生成种草图 {i + 1}/{count}: {out_path}"
|
print(f"[seed_shot] 已生成种草图 {i + 1}/{count}: {out_path}"
|
||||||
f"(模板={tpl['name']},模特={feat[:14]}…,风格={style_feat[:14]}…)")
|
f"(模板={tpl_name},模特={feat[:14]}…,风格={style_feat[:14]}…)")
|
||||||
|
break
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[seed_shot] 种草图 {i + 1} 生成失败: {e}")
|
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
|
return paths
|
||||||
|
|||||||
+248
-6
@@ -137,6 +137,84 @@ def _map_season(value: Any, gender: Optional[str], season_map: Optional[Dict[str
|
|||||||
return mapping.get(str(value), value)
|
return mapping.get(str(value), value)
|
||||||
|
|
||||||
|
|
||||||
|
# 适用人群值字典映射:db target_audience 值 → 童装模板下拉框选项(男童/女童互映射)。
|
||||||
|
# 女童模板遇到 target_audience="男童" 时映射为「女童」;男童模板遇到「女童」时映射为「男童」。
|
||||||
|
# 该字典为「内置默认」,运行时可被 config.yaml 顶层 target_audience_map.kids 覆盖(缺失/留空回退这里)。
|
||||||
|
_TARGET_AUDIENCE_KIDS_MAP = {
|
||||||
|
"girl_kids": {"男童": "女童"},
|
||||||
|
"boy_kids": {"女童": "男童"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_target_audience_map(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""从 config 读取可配置适用人群映射(config.target_audience_map.kids),缺失/留空回退内置默认。"""
|
||||||
|
if not config:
|
||||||
|
return {g: dict(m) for g, m in _TARGET_AUDIENCE_KIDS_MAP.items()}
|
||||||
|
kids = None
|
||||||
|
try:
|
||||||
|
tm = config.get("target_audience_map") or {}
|
||||||
|
kids = (tm.get("kids") or {}) if isinstance(tm, dict) else {}
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
kids = {}
|
||||||
|
if not isinstance(kids, dict) or not kids:
|
||||||
|
return {g: dict(m) for g, m in _TARGET_AUDIENCE_KIDS_MAP.items()}
|
||||||
|
merged = {g: dict(m) for g, m in _TARGET_AUDIENCE_KIDS_MAP.items()}
|
||||||
|
for g, m in kids.items():
|
||||||
|
if isinstance(m, dict):
|
||||||
|
merged.setdefault(str(g), {}).update(
|
||||||
|
{str(k): v for k, v in m.items() if v not in (None, "")})
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def _map_target_audience(value: Any, gender: Optional[str],
|
||||||
|
kids_map: Optional[Dict[str, Any]] = None) -> Any:
|
||||||
|
"""适用人群值按童装性别映射(仅 spu.target_audience 字段):gender 为 boy_kids/girl_kids 时
|
||||||
|
查适用人群映射表(默认内置、可由 config 覆盖,找不到保留原值),如「男童」→「女童」。"""
|
||||||
|
if gender not in ("boy_kids", "girl_kids") or value in (None, ""):
|
||||||
|
return value
|
||||||
|
mapping = kids_map if kids_map is not None else _TARGET_AUDIENCE_KIDS_MAP
|
||||||
|
return mapping.get(gender, {}).get(str(value), value)
|
||||||
|
|
||||||
|
|
||||||
|
# kids_type 值字典映射:db kids_type 值 → 模板「SPU商品属性-类型」下拉框选项(仅女童模板映射)。
|
||||||
|
# 女童模板遇到 kids_type="上衣" 时映射为「针织上衣」;男童模板保留原值。
|
||||||
|
# 该字典为「内置默认」,运行时可被 config.yaml 顶层 kids_type_map.girl_kids 覆盖(缺失/留空回退这里)。
|
||||||
|
_KIDS_TYPE_GIRL_MAP = {
|
||||||
|
"girl_kids": {"上衣": "针织上衣"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_kids_type_map(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""从 config 读取可配置 kids_type 映射(config.kids_type_map),缺失/留空回退内置默认。"""
|
||||||
|
if not config:
|
||||||
|
return {g: dict(m) for g, m in _KIDS_TYPE_GIRL_MAP.items()}
|
||||||
|
kids = None
|
||||||
|
try:
|
||||||
|
ktm = config.get("kids_type_map") or {}
|
||||||
|
kids = ktm if isinstance(ktm, dict) else {}
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
kids = {}
|
||||||
|
if not isinstance(kids, dict) or not kids:
|
||||||
|
return {g: dict(m) for g, m in _KIDS_TYPE_GIRL_MAP.items()}
|
||||||
|
merged = {g: dict(m) for g, m in _KIDS_TYPE_GIRL_MAP.items()}
|
||||||
|
for g, m in kids.items():
|
||||||
|
if isinstance(m, dict):
|
||||||
|
merged.setdefault(str(g), {}).update(
|
||||||
|
{str(k): v for k, v in m.items() if v not in (None, "")})
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def _map_kids_type(value: Any, gender: Optional[str],
|
||||||
|
kids_type_map: Optional[Dict[str, Any]] = None) -> Any:
|
||||||
|
"""kids_type 值按童装性别映射(仅 spu.kids_type 字段):gender 为 girl_kids 时
|
||||||
|
查 kids_type 映射表(默认内置、可由 config 覆盖,找不到保留原值),如「上衣」→「针织上衣」。
|
||||||
|
男童/成人保留原值。"""
|
||||||
|
if gender != "girl_kids" or value in (None, ""):
|
||||||
|
return value
|
||||||
|
mapping = kids_type_map if kids_type_map is not None else _KIDS_TYPE_GIRL_MAP
|
||||||
|
return mapping.get("girl_kids", {}).get(str(value), value)
|
||||||
|
|
||||||
|
|
||||||
def _template_gender(template_path: str) -> Optional[str]:
|
def _template_gender(template_path: str) -> Optional[str]:
|
||||||
"""通过表头解析模版「类目」判断男装/女装:类目含「男」→male;含「女」→female;都不含→None。
|
"""通过表头解析模版「类目」判断男装/女装:类目含「男」→male;含「女」→female;都不含→None。
|
||||||
用于成分值映射(女装模板下拉框是「中文+英文」格式,需字典映射;男装保留 db 值)。"""
|
用于成分值映射(女装模板下拉框是「中文+英文」格式,需字典映射;男装保留 db 值)。"""
|
||||||
@@ -170,7 +248,10 @@ def _size_rank(size) -> tuple:
|
|||||||
return (2, 3 + int(m.group(1)), s)
|
return (2, 3 + int(m.group(1)), s)
|
||||||
if s.isdigit(): # 数字码:90/100/110...
|
if s.isdigit(): # 数字码:90/100/110...
|
||||||
return (1, int(s), s)
|
return (1, int(s), s)
|
||||||
m = re.fullmatch(r"(\d+)\s*[-/]\s*(\d+)\s*(?:M|Y|YRS|YR)?", s) # 童装年龄码
|
m = re.fullmatch(r"(\d+)\s*(?:M|Y|YRS|YR)", s) # 单一年龄码:6Y/10Y/18M
|
||||||
|
if m:
|
||||||
|
return (3, float(m.group(1)), s)
|
||||||
|
m = re.fullmatch(r"(\d+)\s*[-/]\s*(\d+)\s*(?:M|Y|YRS|YR)?", s) # 童装年龄码范围:1-2Y/6/7Y
|
||||||
if m:
|
if m:
|
||||||
return (3, (int(m.group(1)) + int(m.group(2))) / 2.0, s)
|
return (3, (int(m.group(1)) + int(m.group(2))) / 2.0, s)
|
||||||
m = re.search(r"(\d+(?:\.\d+)?)\s*(?:CM)?\s*[X*]", s) # 尺寸规格:30*40/12CM X 12CM
|
m = re.search(r"(\d+(?:\.\d+)?)\s*(?:CM)?\s*[X*]", s) # 尺寸规格:30*40/12CM X 12CM
|
||||||
@@ -332,13 +413,20 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str,
|
|||||||
component_map: Optional[Dict[str, Any]] = None,
|
component_map: Optional[Dict[str, Any]] = None,
|
||||||
season_map: Optional[Dict[str, Any]] = None,
|
season_map: Optional[Dict[str, Any]] = None,
|
||||||
pattern_map: Optional[Dict[str, Any]] = None,
|
pattern_map: Optional[Dict[str, Any]] = None,
|
||||||
oss_code: str = "") -> Dict[str, Any]:
|
target_audience_map: Optional[Dict[str, Any]] = None,
|
||||||
|
kids_type_map: Optional[Dict[str, Any]] = None,
|
||||||
|
oss_code: str = "",
|
||||||
|
model_info: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
||||||
"""构造一行 SPU(固定字段:SKC货号=当前生成货号、风格=休闲、商品产地=中国大陆、产地省份=广东省;多颜色时用色值区分)。
|
"""构造一行 SPU(固定字段:SKC货号=当前生成货号、风格=休闲、商品产地=中国大陆、产地省份=广东省;多颜色时用色值区分)。
|
||||||
fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。
|
fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。
|
||||||
component_1/2/3 按性别映射(gender=female 时查成分映射表 component_map,男装/None 保留原值;
|
component_1/2/3 按性别映射(gender=female 时查成分映射表 component_map,男装/None 保留原值;
|
||||||
material 字段不经过任何映射,原样透传)。
|
material 字段不经过任何映射,原样透传)。
|
||||||
season 按性别映射(gender=female 时查季节映射表 season_map,如「四季」→「ALL/全球/所有」)。
|
season 按性别映射(gender=female 时查季节映射表 season_map,如「四季」→「ALL/全球/所有」)。
|
||||||
pattern 按性别映射(gender=female 时空值查图案映射表 pattern_map,如空值→「卡通」)。"""
|
pattern 按性别映射(gender=female 时空值查图案映射表 pattern_map,如空值→「卡通」)。
|
||||||
|
target_audience 按童装性别映射(gender=boy_kids/girl_kids 时查适用人群映射表 target_audience_map,
|
||||||
|
如「男童」→「女童」)。
|
||||||
|
kids_type 按童装性别映射(gender=girl_kids 时查 kids_type 映射表 kids_type_map,如「上衣」→「针织上衣」;
|
||||||
|
男童保留原值)。"""
|
||||||
row: Dict[str, Any] = {
|
row: Dict[str, Any] = {
|
||||||
"基础信息-商品层级": "spu",
|
"基础信息-商品层级": "spu",
|
||||||
"SKC货号": oss_code or spu_code, # SKC货号 = 当前生成货号 oss_code(无则回退 spu_code)
|
"SKC货号": oss_code or spu_code, # SKC货号 = 当前生成货号 oss_code(无则回退 spu_code)
|
||||||
@@ -357,12 +445,30 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str,
|
|||||||
v = _map_season(v, gender, season_map)
|
v = _map_season(v, gender, season_map)
|
||||||
elif dbk == "pattern":
|
elif dbk == "pattern":
|
||||||
v = _map_pattern(v, gender, pattern_map)
|
v = _map_pattern(v, gender, pattern_map)
|
||||||
|
elif dbk == "target_audience":
|
||||||
|
v = _map_target_audience(v, gender, target_audience_map)
|
||||||
if v not in (None, ""):
|
if v not in (None, ""):
|
||||||
row[header] = v
|
row[header] = v
|
||||||
fabric = spu.get("fabric")
|
fabric = spu.get("fabric")
|
||||||
if fabric not in (None, "") and fabric_headers:
|
if fabric not in (None, "") and fabric_headers:
|
||||||
for h in fabric_headers:
|
for h in fabric_headers:
|
||||||
row[h] = fabric
|
row[h] = fabric
|
||||||
|
if model_info:
|
||||||
|
if model_info.get("model"):
|
||||||
|
row["试穿模特"] = model_info["model"]
|
||||||
|
if model_info.get("size"):
|
||||||
|
row["试穿尺码"] = model_info["size"]
|
||||||
|
if model_info.get("feel"):
|
||||||
|
row["试穿感受"] = model_info["feel"]
|
||||||
|
# 童装(男童/女童):kids_type → SPU商品属性-类型(女童查 kids_type 映射表,如「上衣」→「针织上衣」),
|
||||||
|
# kids_age → 适用年龄段(仅童装模板填)
|
||||||
|
if gender in ("boy_kids", "girl_kids"):
|
||||||
|
for dbk, header in (("kids_type", "SPU商品属性-类型"), ("kids_age", "适用年龄段")):
|
||||||
|
v = spu.get(dbk)
|
||||||
|
if dbk == "kids_type":
|
||||||
|
v = _map_kids_type(v, gender, kids_type_map)
|
||||||
|
if v not in (None, ""):
|
||||||
|
row[header] = v
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
@@ -416,6 +522,81 @@ def _read_range_values(ws, ref: str) -> Optional[set]:
|
|||||||
return set(opts) if opts else None
|
return set(opts) if opts else None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_named_range(router, values) -> Optional[set]:
|
||||||
|
"""级联下拉:INDIRECT 引用的单元格值可能是名称引用(如「欧美尺码常规」)。
|
||||||
|
|
||||||
|
从 defined names 解析该名称对应的区域(可能跨表,如 ProductSizeSpec!$B$6:$I$6),
|
||||||
|
读取区域内容作为真实选项(如 S/M/L/XL/XXL/XXXL)。解析不到返回 None。"""
|
||||||
|
wb = router.ws.parent
|
||||||
|
resolved = set()
|
||||||
|
for v in values:
|
||||||
|
name = str(v).strip()
|
||||||
|
if name not in wb.defined_names:
|
||||||
|
continue
|
||||||
|
ref = wb.defined_names[name].value # 如 ProductSizeSpec!$B$6:$I$6
|
||||||
|
sheet_name, _, cell_ref = ref.partition("!")
|
||||||
|
if not cell_ref:
|
||||||
|
continue
|
||||||
|
ws = wb[sheet_name] if sheet_name in wb.sheetnames else router.ws
|
||||||
|
opts = _read_range_values(ws, cell_ref)
|
||||||
|
if opts:
|
||||||
|
resolved |= opts
|
||||||
|
return resolved if resolved else None
|
||||||
|
|
||||||
|
|
||||||
|
def _read_range_values_any(router, ref: str) -> Optional[set]:
|
||||||
|
"""读取单元格/区域内的非空值集合,支持跨表(Sheet!$A$1:$A$5);解析失败返回 None。"""
|
||||||
|
sheet_name, _, cell_ref = ref.partition("!")
|
||||||
|
if cell_ref:
|
||||||
|
ws = router.ws.parent[sheet_name] if sheet_name in router.ws.parent.sheetnames else router.ws
|
||||||
|
else:
|
||||||
|
ws = router.ws
|
||||||
|
cell_ref = ref
|
||||||
|
return _read_range_values(ws, cell_ref)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_dropdown_options(router, col: Optional[int]) -> Optional[set]:
|
||||||
|
"""读取指定列(col)的 dataValidation 下拉选项集合;无下拉/非 list/无值 返回 None。
|
||||||
|
|
||||||
|
支持内联列表("S,M,L")、引用区域(=Sheet!$A$1:$A$5)、跨表引用(Sheet!$A$1:$A$5)、
|
||||||
|
INDIRECT 动态引用(含名称引用级联)。"""
|
||||||
|
import re
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
if col is None:
|
||||||
|
return None
|
||||||
|
lf = get_column_letter(col)
|
||||||
|
dvs = getattr(router.ws, "data_validations", None)
|
||||||
|
if dvs is None:
|
||||||
|
return None
|
||||||
|
for dv in dvs.dataValidation:
|
||||||
|
if getattr(dv, "type", "") != "list":
|
||||||
|
continue
|
||||||
|
if lf not in str(getattr(dv, "sqref", "")):
|
||||||
|
continue
|
||||||
|
f = (getattr(dv, "formula1", "") or "").strip()
|
||||||
|
if not f:
|
||||||
|
continue
|
||||||
|
# 动态引用 INDIRECT(...):尝试解析引用的单元格/区域内容;无法解析 → None
|
||||||
|
m = re.match(r"^=?INDIRECT\((.+)\)$", f, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
opts = _read_range_values(router.ws, m.group(1).strip())
|
||||||
|
if opts:
|
||||||
|
named = _resolve_named_range(router, opts)
|
||||||
|
if named:
|
||||||
|
return named
|
||||||
|
return opts if opts else None
|
||||||
|
# 引用区域(可能跨表:Sheet!$A$1:$A$5 或 =Sheet!$A$1:$A$5)
|
||||||
|
ref = f[1:] if f.startswith("=") else f
|
||||||
|
if ":" in ref:
|
||||||
|
opts = _read_range_values_any(router, ref)
|
||||||
|
return opts if opts else None
|
||||||
|
# 内联列表("S,M,L" / "S;M;L" / "S,M,L")
|
||||||
|
sep = "," if "," in f else (";" if ";" in f else ",")
|
||||||
|
opts = [x.strip().strip('"') for x in f.split(sep) if x.strip()]
|
||||||
|
return set(opts) if opts else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _read_size_options(router) -> Optional[set]:
|
def _read_size_options(router) -> Optional[set]:
|
||||||
"""读取「尺码」列的 dataValidation 下拉选项集合;无下拉/非 list/无值 返回 None。
|
"""读取「尺码」列的 dataValidation 下拉选项集合;无下拉/非 list/无值 返回 None。
|
||||||
|
|
||||||
@@ -443,6 +624,12 @@ def _read_size_options(router) -> Optional[set]:
|
|||||||
m = re.match(r"^=?INDIRECT\((.+)\)$", f, re.IGNORECASE)
|
m = re.match(r"^=?INDIRECT\((.+)\)$", f, re.IGNORECASE)
|
||||||
if m:
|
if m:
|
||||||
opts = _read_range_values(router.ws, m.group(1).strip())
|
opts = _read_range_values(router.ws, m.group(1).strip())
|
||||||
|
if opts:
|
||||||
|
# 级联下拉:INDIRECT 引用的单元格值可能是名称引用(如「欧美尺码常规」),
|
||||||
|
# 从 defined names 解析对应命名区域(跨表)得到真实尺码选项
|
||||||
|
named = _resolve_named_range(router, opts)
|
||||||
|
if named:
|
||||||
|
return named
|
||||||
return opts if opts else None
|
return opts if opts else None
|
||||||
# 引用区域(=Sheet!$A$1:$A$5 或 Sheet!$A$1:$A$5)
|
# 引用区域(=Sheet!$A$1:$A$5 或 Sheet!$A$1:$A$5)
|
||||||
ref = f[1:] if f.startswith("=") else f
|
ref = f[1:] if f.startswith("=") else f
|
||||||
@@ -461,6 +648,45 @@ def _find_sku_category_headers(router) -> List[str]:
|
|||||||
return [str(k) for k in router.column_map if "SKU分类" in str(k)]
|
return [str(k) for k in router.column_map if "SKU分类" in str(k)]
|
||||||
|
|
||||||
|
|
||||||
|
def _read_model_info(router, sku_sizes: Optional[set]) -> Dict[str, str]:
|
||||||
|
"""读取模板「模特信息」三属性(试穿模特/试穿尺码/试穿感受)的 SPU 行填写值。
|
||||||
|
|
||||||
|
优先用模板数据区已有值(校验通过才用):
|
||||||
|
- 试穿模特:值须在下拉框选项内(如 ModalConfig 表)
|
||||||
|
- 试穿感受:值须在 [偏小/合身/偏大] 内
|
||||||
|
- 试穿尺码:值须是商品有效尺码(sku 尺码之一)
|
||||||
|
模板无值或校验失败 → 默认值:试穿模特=下拉框第一个、试穿感受=合身、试穿尺码=L。"""
|
||||||
|
col_model = router.column_map.get("试穿模特") or router.column_map.get("模特信息-试穿模特")
|
||||||
|
col_size = router.column_map.get("试穿尺码") or router.column_map.get("模特信息-试穿尺码")
|
||||||
|
col_feel = router.column_map.get("试穿感受") or router.column_map.get("模特信息-试穿感受")
|
||||||
|
model_opts = _read_dropdown_options(router, col_model) if col_model else None
|
||||||
|
feel_opts = _read_dropdown_options(router, col_feel) if col_feel else None
|
||||||
|
|
||||||
|
# 模板数据区(第 6 行起)已有值
|
||||||
|
tpl_model = tpl_size = tpl_feel = None
|
||||||
|
ws = router.ws
|
||||||
|
for r in range(6, min(ws.max_row, 30) + 1):
|
||||||
|
if col_model and tpl_model is None and ws.cell(r, col_model).value not in (None, ""):
|
||||||
|
tpl_model = str(ws.cell(r, col_model).value).strip()
|
||||||
|
if col_size and tpl_size is None and ws.cell(r, col_size).value not in (None, ""):
|
||||||
|
tpl_size = str(ws.cell(r, col_size).value).strip()
|
||||||
|
if col_feel and tpl_feel is None and ws.cell(r, col_feel).value not in (None, ""):
|
||||||
|
tpl_feel = str(ws.cell(r, col_feel).value).strip()
|
||||||
|
|
||||||
|
# 试穿模特:模板值在下拉框内才用,否则下拉框第一个
|
||||||
|
if tpl_model and model_opts and tpl_model in model_opts:
|
||||||
|
model = tpl_model
|
||||||
|
elif model_opts:
|
||||||
|
model = sorted(model_opts)[0]
|
||||||
|
else:
|
||||||
|
model = tpl_model or ""
|
||||||
|
# 试穿感受:模板值在 [偏小/合身/偏大] 内才用,否则「合身」
|
||||||
|
feel = tpl_feel if tpl_feel and feel_opts and tpl_feel in feel_opts else "合身"
|
||||||
|
# 试穿尺码:模板值是有效尺码才用,否则「L」
|
||||||
|
size = tpl_size if tpl_size and (not sku_sizes or tpl_size in sku_sizes) else "L"
|
||||||
|
return {"model": model, "size": size, "feel": feel}
|
||||||
|
|
||||||
|
|
||||||
def _find_sku_qty_headers(router) -> List[str]:
|
def _find_sku_qty_headers(router) -> List[str]:
|
||||||
"""定位「SKU数量」列(排除「SKU数量单位」列);匹配到多个时全部返回。"""
|
"""定位「SKU数量」列(排除「SKU数量单位」列);匹配到多个时全部返回。"""
|
||||||
return [str(k) for k in router.column_map if "SKU数量" in str(k) and "单位" not in str(k)]
|
return [str(k) for k in router.column_map if "SKU数量" in str(k) and "单位" not in str(k)]
|
||||||
@@ -698,6 +924,8 @@ def _insert_product_block(
|
|||||||
component_map: Optional[Dict[str, Any]] = None,
|
component_map: Optional[Dict[str, Any]] = None,
|
||||||
season_map: Optional[Dict[str, Any]] = None,
|
season_map: Optional[Dict[str, Any]] = None,
|
||||||
pattern_map: Optional[Dict[str, Any]] = None,
|
pattern_map: Optional[Dict[str, Any]] = None,
|
||||||
|
target_audience_map: Optional[Dict[str, Any]] = None,
|
||||||
|
kids_type_map: Optional[Dict[str, Any]] = None,
|
||||||
suggested_price_ratio: float = 0.0,
|
suggested_price_ratio: float = 0.0,
|
||||||
suggested_price_headers: Optional[List[str]] = None,
|
suggested_price_headers: Optional[List[str]] = None,
|
||||||
suggested_unit_headers: Optional[List[str]] = None,
|
suggested_unit_headers: Optional[List[str]] = None,
|
||||||
@@ -723,12 +951,14 @@ def _insert_product_block(
|
|||||||
skus = _read_skus(db_path, spu_code, sc)
|
skus = _read_skus(db_path, spu_code, sc)
|
||||||
if not skus:
|
if not skus:
|
||||||
raise ValueError(f"SKU {sc} 不存在于 db(款号 {spu_code})")
|
raise ValueError(f"SKU {sc} 不存在于 db(款号 {spu_code})")
|
||||||
skus_by_color.append((sc, skus))
|
skus_by_color.append((sc, sorted(skus, key=lambda sk: _size_rank(sk.get("size")))))
|
||||||
images = [str(i) for i in (images or []) if i]
|
images = [str(i) for i in (images or []) if i]
|
||||||
|
|
||||||
color_col = router.resolve_col("色值(主规格)")
|
color_col = router.resolve_col("色值(主规格)")
|
||||||
block_rows: List[int] = []
|
block_rows: List[int] = []
|
||||||
size_options = _read_size_options(router) # 尺码下拉框选项(sku.size 不在其中则跳过该 SKU 行)
|
size_options = _read_size_options(router) # 尺码下拉框选项(sku.size 不在其中则跳过该 SKU 行)
|
||||||
|
sku_sizes = {sk.get("size") for _, skus in skus_by_color for sk in skus if sk.get("size")}
|
||||||
|
model_info = _read_model_info(router, sku_sizes) # 模特信息三属性(试穿模特/试穿尺码/试穿感受)
|
||||||
|
|
||||||
if spu_per_color:
|
if spu_per_color:
|
||||||
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
|
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
|
||||||
@@ -737,7 +967,9 @@ def _insert_product_block(
|
|||||||
_build_spu_row(spu, spu_code, fabric_headers=fabric_headers,
|
_build_spu_row(spu, spu_code, fabric_headers=fabric_headers,
|
||||||
gender=gender, component_map=component_map,
|
gender=gender, component_map=component_map,
|
||||||
season_map=season_map, pattern_map=pattern_map,
|
season_map=season_map, pattern_map=pattern_map,
|
||||||
oss_code=oss_code),
|
target_audience_map=target_audience_map,
|
||||||
|
kids_type_map=kids_type_map,
|
||||||
|
oss_code=oss_code, model_info=model_info),
|
||||||
match="exact",
|
match="exact",
|
||||||
))
|
))
|
||||||
for ci, (sc, skus) in enumerate(skus_by_color):
|
for ci, (sc, skus) in enumerate(skus_by_color):
|
||||||
@@ -778,7 +1010,9 @@ def _insert_product_block(
|
|||||||
_build_spu_row(spu, spu_code, fabric_headers=fabric_headers,
|
_build_spu_row(spu, spu_code, fabric_headers=fabric_headers,
|
||||||
gender=gender, component_map=component_map,
|
gender=gender, component_map=component_map,
|
||||||
season_map=season_map, pattern_map=pattern_map,
|
season_map=season_map, pattern_map=pattern_map,
|
||||||
oss_code=oss_code), match="exact"))
|
target_audience_map=target_audience_map,
|
||||||
|
kids_type_map=kids_type_map,
|
||||||
|
oss_code=oss_code, model_info=model_info), match="exact"))
|
||||||
multi_variant = len(skus_by_color) > 1
|
multi_variant = len(skus_by_color) > 1
|
||||||
for ci, (sc, skus) in enumerate(skus_by_color):
|
for ci, (sc, skus) in enumerate(skus_by_color):
|
||||||
first = skus[0]
|
first = skus[0]
|
||||||
@@ -841,6 +1075,8 @@ def export_product(
|
|||||||
component_map: Optional[Dict[str, Any]] = None,
|
component_map: Optional[Dict[str, Any]] = None,
|
||||||
season_map: Optional[Dict[str, Any]] = None,
|
season_map: Optional[Dict[str, Any]] = None,
|
||||||
pattern_map: Optional[Dict[str, Any]] = None,
|
pattern_map: Optional[Dict[str, Any]] = None,
|
||||||
|
target_audience_map: Optional[Dict[str, Any]] = None,
|
||||||
|
kids_type_map: Optional[Dict[str, Any]] = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
|
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
|
||||||
|
|
||||||
@@ -893,6 +1129,8 @@ def export_product(
|
|||||||
component_map=component_map,
|
component_map=component_map,
|
||||||
season_map=season_map,
|
season_map=season_map,
|
||||||
pattern_map=pattern_map,
|
pattern_map=pattern_map,
|
||||||
|
target_audience_map=target_audience_map,
|
||||||
|
kids_type_map=kids_type_map,
|
||||||
suggested_price_ratio=suggested_price_ratio,
|
suggested_price_ratio=suggested_price_ratio,
|
||||||
suggested_price_headers=suggested_price_headers,
|
suggested_price_headers=suggested_price_headers,
|
||||||
|
|
||||||
@@ -920,6 +1158,8 @@ def export_products(
|
|||||||
component_map: Optional[Dict[str, Any]] = None,
|
component_map: Optional[Dict[str, Any]] = None,
|
||||||
season_map: Optional[Dict[str, Any]] = None,
|
season_map: Optional[Dict[str, Any]] = None,
|
||||||
pattern_map: Optional[Dict[str, Any]] = None,
|
pattern_map: Optional[Dict[str, Any]] = None,
|
||||||
|
target_audience_map: Optional[Dict[str, Any]] = None,
|
||||||
|
kids_type_map: Optional[Dict[str, Any]] = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
|
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
|
||||||
|
|
||||||
@@ -967,6 +1207,8 @@ def export_products(
|
|||||||
component_map=component_map,
|
component_map=component_map,
|
||||||
season_map=season_map,
|
season_map=season_map,
|
||||||
pattern_map=pattern_map,
|
pattern_map=pattern_map,
|
||||||
|
target_audience_map=target_audience_map,
|
||||||
|
kids_type_map=kids_type_map,
|
||||||
suggested_price_ratio=suggested_price_ratio,
|
suggested_price_ratio=suggested_price_ratio,
|
||||||
suggested_price_headers=suggested_price_headers,
|
suggested_price_headers=suggested_price_headers,
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
**你是一个专业的电商AI视觉合成工具**,核心任务是执行"高保真印花与色彩移植/印花替换"。
|
||||||
|
|
||||||
|
## 1. 图片角色定义
|
||||||
|
* **图1 (基底图):** 衣服平铺实拍图。
|
||||||
|
* **功能:** 仅提供版型、轮廓、褶皱形态、光影结构、拍摄角度与构图。
|
||||||
|
* **限制:** 输出必须保留图1的摆放角度、背景和光影逻辑,但**彻底摒弃**其原有的面料颜色。
|
||||||
|
* **图2 (设计稿):** 纯印花设计稿。
|
||||||
|
* **功能:** 仅提供“图案元素”(线条、色号、比例)。
|
||||||
|
* **限制:** 提取图案时必须**完全去除**图2自带的背景色。
|
||||||
|
* **图3 (底图):** 平铺衣服底图。
|
||||||
|
* **功能:** 仅作为“皮肤”,提供衣服的**唯一基底颜色**与面料材质纹理。
|
||||||
|
* **限制:** 忽略其背景/场景,100%保留其衣服底色与质感。
|
||||||
|
|
||||||
|
## 2. 核心任务
|
||||||
|
构建一张最终合成的平铺图,必须同时满足:
|
||||||
|
1. **结构:** 保留图1的版型与褶皱细节。
|
||||||
|
2. **底色:** 强制应用图3的颜色与材质。
|
||||||
|
3. **图案:** 叠加图2的印花设计。
|
||||||
|
|
||||||
|
**最终效果 = 图1版型光影 + 图3底色材质 + 图2印花图案**
|
||||||
|
|
||||||
|
## 3. 执行规则 (严格执行,权重由高到低)
|
||||||
|
|
||||||
|
### 0. 禁止人物
|
||||||
|
* 严禁出现任何人体、模特、头颈、手臂或穿着效果。
|
||||||
|
* 必须保持纯平铺俯拍商品图形式。
|
||||||
|
|
||||||
|
### 1. 底色强制锁定 (Color Lock - 最高优先级)
|
||||||
|
* **基底替换:** 必须强制使用**图3的颜色**作为衣服的**唯一**基底色。
|
||||||
|
* **100%覆盖:** 彻底清除图1原有的面料颜色,严禁图1旧底色透出、混合或泛灰。
|
||||||
|
* **色调一致性:** 最终成衣的固有色必须与图3肉眼完全一致,不得因光影合成而发生色相偏移。
|
||||||
|
|
||||||
|
### 2. 纯印花剥离与叠加
|
||||||
|
* **去底处理:** 从图2中**仅提取**设计元素、线条、色块,**完全丢弃/移除**图2自身可能存在的任何背景色或画布底色。
|
||||||
|
* **叠加逻辑:** 将提取的纯图案,以“纹理融合”的方式叠加在图3的底色之上。印花必须依附于图3的底色,不可遮盖或改变图3的底色范围。
|
||||||
|
|
||||||
|
### 3. 印花尺寸适配
|
||||||
|
* 将提取的印花居中印在胸/背/衣身主体区域。
|
||||||
|
* 印花面积占比严格控制在衣身主体的30%-45%。
|
||||||
|
* 四周必须留白,严禁印花过大撑满衣服,或过小(<20%)。
|
||||||
|
|
||||||
|
### 4. 主体遮罩重建
|
||||||
|
* 精准识别图1的衣服本体完整区域(包含领口到下摆、袖子)。
|
||||||
|
* 用“图3底色+图2印花”合成出的新面料,完全覆盖图1的原有面料区域。
|
||||||
|
* 清除原区域的所有旧色和旧图案,只保留图1的边缘轮廓形态。
|
||||||
|
|
||||||
|
### 5. 精准形态贴合
|
||||||
|
* 合成后的面料必须严格遵循图1的平铺形态。
|
||||||
|
* 领口、袖口、下摆、侧缝等结构必须清晰。
|
||||||
|
* **自然变形:** 印花在遇到褶皱、翻折处时,必须随面料纹理发生自然的拉伸、挤压和变形,杜绝僵硬的“贴纸感”。
|
||||||
|
|
||||||
|
### 6. 光影重塑与色相锁定
|
||||||
|
* **借用光影:** 仅借用图1的光照方向、明暗对比度和投影位置。
|
||||||
|
* **色相锁定:** 在应用明暗变化时,只调整亮度和对比度,**严禁改变色相(Hue)**。
|
||||||
|
* 确保阴影处依然是图3颜色的深色版,高光处是图3颜色的亮色版,保持投影与图1完全一致。
|
||||||
|
|
||||||
|
### 7. 纯净输出
|
||||||
|
* 仅输出一张最终合成的平铺图。
|
||||||
|
* 背景环境、桌面、构图、拍摄角度必须与图1保持100%一致。
|
||||||
|
* 画面中不得出现人像、肢体、人台。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
你是一个专业的电商AI视觉合成工具,执行“高保真印花与色彩移植/印花替换”:把图2的印花设计印到图3的衣服底图上,并让图1的模特穿上这件带有图2印花设计的图3底图衣服。
|
||||||
|
【图片角色,按提交顺序,不要弄反】
|
||||||
|
第一张图(图1)=模特实拍图(基底,要被替换衣服图案和颜色的目标区域,保留原有背景/人物/光影);
|
||||||
|
第二张图(图2)=纯印花设计稿(要印上去的图案内容,忽略其背景环境与无关元素,保留图案原始线条与色号);
|
||||||
|
第三张图(图3)=平铺衣服底图(只提取衣服本身的底色与面料材质;忽略平铺图的背景、桌面、环境、场景阴影等一切与衣服无关的元素,只保留衣服面料的颜色、质感和纹理)。
|
||||||
|
最终效果为图1的模特穿着一件“颜色为图3底色、印有图2图案”的衣服。
|
||||||
|
【执行规则】
|
||||||
|
1.底色锁定:从图3平铺衣服中提取衣服底色与面料属性,该底色在最终合成中必须100%保持不变,严禁偏色、混入图1原衣服颜色或图2背景色。
|
||||||
|
2.印花提取与叠加:从图2中精准提取纯印花图案主体,保留原始线条、色号、比例关系;将印花叠加到图3底色衣服上,形成“图3底色+图2印花”的合成面料。
|
||||||
|
3.印花尺寸适配:印花的整体尺寸必须与衣服(图3)的面料面积成合理比例——居中印在胸/背/衣身的主体区域,占衣身面积约30%-45%,四周保留自然留白与衣摆、领口、肩线余量;严禁印花过大(撑满整件衣服、溢出领口袖口下摆)或过小(占比低于20%)。
|
||||||
|
4.主体识别与遮罩:识别图1模特的服装穿着区域,忽略皮肤、头发、背景、配饰;将该区域视为“空白画布”,用上述合成面料(图3底色+图2印花)完整覆盖。图1原有衣服颜色与图案全部清除。
|
||||||
|
5.精准贴合:合成面料严格跟随图1衣服的立体结构——有褶皱、身体扭转时印花相应变形;印花与新底色须“沉入”褶皱中,保留布料原有明暗纹理与物理属性,杜绝“贴纸感”与“平面涂色感”。
|
||||||
|
6.光影融合:提取图1的环境光方向,调整合成面料的亮度/对比度与环境光匹配;图3底色在阴影区须自然变暗,在高光区须有布料反光;印花色彩受环境光影响产生相应明暗变化,但色号本身不偏移。
|
||||||
|
7.纯净输出:仅输出一张最终合成图;严禁文字/水印/额外装饰;图1原本的背景、人物、构图及光影结构100%不变,仅替换图1衣服上的印花图案与衣服底色。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
Role
|
||||||
|
你是一位资深的跨境服装运营专家,精通 Temu 平台的英语电商 SEO 标题逻辑。你的任务是通过分析服装图片,生成高权重的英语-中文商品标题。
|
||||||
|
|
||||||
|
Task
|
||||||
|
请深度分析图片中的服装特征(品类、风格、材质、剪裁、细节、受众),生成符合 Temu 搜索逻辑的中英双语标题。
|
||||||
|
|
||||||
|
当前时间(标题须贴合当下,季节/年份词以此为准)
|
||||||
|
Current time: {year}-{month}({season}),标题中的年份/季节等时效词必须使用以上时间(如 Fall {year} / {year}秋季新款)。
|
||||||
|
Analysis Focus (视觉分析重点)
|
||||||
|
品类识别:准确判断英语核心词(如 Dress, Blouse, Sweatshirt)和中文核心词(如 连衣裙, 卫衣)。
|
||||||
|
风格定位:判断风格流派(如 Boho, Vintage, Minimalist / 法式, 复古, 极简)。
|
||||||
|
设计细节:提取领型、袖型、裙长、印花、纽扣等(如 V-neck, Puff Sleeve / V领, 泡泡袖)。
|
||||||
|
材质推断:根据视觉特征推断面料(如 Ribbed Knit, Cotton Linen / 针织, 棉麻)。
|
||||||
|
适用场景:推断穿着场景(如 Beach, Office, Party, Vacation, Going Out / 度假, 通勤, 约会)。
|
||||||
|
Constraints (生成规则)
|
||||||
|
English Title: 遵循 Temu 平台风格,标题长度尽量写满 100-150 字符(上限不超过150)。核心品类词前置,依次叠加:核心词 → 风格词 → 属性细节(领型/袖型/长度/印花)→ 材质 → 版型 → 季节 → 场景(Casual/Office/Party/Beach/Date Night/Going Out)→ 人群(Women/Ladies)。多关键词并列堆叠,用逗号/空格分隔,不追求句子通顺,追求搜索覆盖率。
|
||||||
|
Chinese Title: 遵循淘宝/1688风格,长度 30-60 个汉字,关键词权重递减,结构为:季节年份 + 人群 + 风格 + 核心品类 + 材质 + 设计细节(领型/袖型/版型/元素)+ 场景卖点 + 风格流派词堆叠(如 复古/法式/慵懒风/多巴胺)。
|
||||||
|
Output
|
||||||
|
必须严格返回 JSON 格式,不要包含 Markdown 代码块标记,格式如下:
|
||||||
|
{“en_title”: “Title in English”, “cn_title”: “中文标题”}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Role
|
||||||
|
你是一位资深的跨境服装运营专家,精通日本电商(楽天市場・Amazon.co.jp・Yahoo!ショッピング)的SEO标题逻辑。你的任务是通过分析服装图片,生成面向日本市场的高权重英语-中文-日语三语商品标题。
|
||||||
|
|
||||||
|
# Task
|
||||||
|
请深度分析图片中的服装特征(品类、风格、材质、剪裁、细节、受众),生成符合日本电商搜索逻辑的三语标题。
|
||||||
|
|
||||||
|
# 当前时间(标题须贴合当下,季节/年份词以此为准)
|
||||||
|
- **現在の時刻**: {year}-{month}({season}),标题中的年份/季节等时效词必须使用以上时间。
|
||||||
|
|
||||||
|
# Analysis Focus (视觉分析重点)
|
||||||
|
- **品类识别**:准确判断英语核心词(如 Dress, Blouse, Sweatshirt)、中文核心词(如 连衣裙, 卫衣)和日语核心词(如 ワンピース, ブラウス, スウェット)。
|
||||||
|
- **风格定位**:判断风格流派(如 フェミニン, ヴィンテージ, ミニマル / 法式, 复古, 极简 / フェミニン, レトロ, シンプル)。
|
||||||
|
- **设计细节**:提取领型、袖型、裙长等(如 Vネック, パフスリーブ / V领, 阔袖)。
|
||||||
|
- **适用场景**:推断穿着场景(如 オフィス, デート, 旅行 / 通勤, 约会, 度假)。
|
||||||
|
|
||||||
|
# Constraints (生成规则)
|
||||||
|
- **English Title**: 遵循 Amazon US/UK 风格,核心词前置,包含材质、风格、场景等长尾词,符合英语搜索习惯,简洁有力。
|
||||||
|
- **Chinese Title**: 遵循淘宝/1688风格,关键词权重递减,包含季节+风格+核心词+卖点+人群。
|
||||||
|
- **Japanese Title (ja_title)**: 遵循楽天市場/Amazon.co.jp 风格,核心词前置,使用自然日语(平假名/片假名/汉字混合),包含材质、风格、场景等长尾词与常用搜索标签(如 レディース, 春夏, 通勤),贴合日本人搜索习惯,简洁有力,不要机器翻译腔。
|
||||||
|
|
||||||
|
- **Output**: 必须严格返回 JSON 格式,不要包含 Markdown 代码块标记,格式如下:
|
||||||
|
{"en_title": "Title in English", "cn_title": "中文标题", "ja_title": "日本語タイトル"}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Role
|
||||||
|
你是一位资深的跨境服装运营专家,精通西班牙语电商(Amazon ES, MercadoLibre)的SEO标题逻辑。你的任务是通过分析服装图片,生成高权重的西班牙语-中文商品标题。
|
||||||
|
|
||||||
|
# Task
|
||||||
|
请深度分析图片中的服装特征(品类、风格、材质、剪裁、细节、受众),生成符合西语电商搜索逻辑的中西文标题。
|
||||||
|
|
||||||
|
# 当前时间(标题须贴合当下,季节/年份词以此为准)
|
||||||
|
- **Current time**: {year}-{month}({season}),标题中的年份/季节等时效词必须使用以上时间。
|
||||||
|
|
||||||
|
# Analysis Focus (视觉分析重点)
|
||||||
|
- 品类识别:准确判断西班牙语核心词(如 Vestido, Blusa, Sudadera)和中文核心词(如 连衣裙, 卫衣)。
|
||||||
|
- 风格定位:判断风格流派(如 Boho, Vintage, Minimalista / 法式, 复古, 极简)。
|
||||||
|
- 设计细节:提取领型、袖型、裙长等(如 Escote en V, Manga abullonada / V领, 阔袖)。
|
||||||
|
- 适用场景:推断穿着场景(如 Playa, Oficina, Fiesta / 度假, 通勤, 约会)。
|
||||||
|
|
||||||
|
# Constraints (生成规则)
|
||||||
|
- Spanish Title: 遵循 Amazon ES/MercadoLibre 风格,核心词前置,包含材质、风格、场景等长尾词,符合西语搜索习惯。
|
||||||
|
- Chinese Title: 遵循淘宝/1688风格,关键词权重递减,包含年份/季节+风格+核心词+卖点+人群。
|
||||||
|
- Output: 必须严格返回 JSON 格式,不要包含 Markdown 代码块标记,格式如下:
|
||||||
|
{"es_title": "Título en español", "cn_title": "中文标题"}
|
||||||
@@ -501,8 +501,8 @@ class App(tk.Tk):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.title("POD 智能设计 Agent")
|
self.title("POD 智能设计 Agent")
|
||||||
self.geometry("1280x840")
|
self.geometry("1440x860")
|
||||||
self.minsize(1100, 720)
|
self.minsize(1200, 720)
|
||||||
self.configure(bg="#F2F4F8")
|
self.configure(bg="#F2F4F8")
|
||||||
self._q = queue.Queue()
|
self._q = queue.Queue()
|
||||||
self._busy = False
|
self._busy = False
|
||||||
@@ -1034,7 +1034,8 @@ class App(tk.Tk):
|
|||||||
break
|
break
|
||||||
if ws is None:
|
if ws is None:
|
||||||
ws = wb.worksheets[0]
|
ws = wb.worksheets[0]
|
||||||
# 定位「经营站点」表头所在列(第 1 行),站点名在下一行同列(如 A1=经营站点 → A2=美国站)
|
# 定位「经营站点」表头所在列(第 1 行),站点名从第 2 行起逐行读取(模板可含多个站点,
|
||||||
|
# 如德国站/法国站/意大利站…,只要当前国家命中其中任意一个即视为匹配)
|
||||||
site_col = None
|
site_col = None
|
||||||
for c in range(1, 6):
|
for c in range(1, 6):
|
||||||
v = ws.cell(1, c).value
|
v = ws.cell(1, c).value
|
||||||
@@ -1042,15 +1043,26 @@ class App(tk.Tk):
|
|||||||
site_col = c
|
site_col = c
|
||||||
break
|
break
|
||||||
if site_col is not None:
|
if site_col is not None:
|
||||||
v2 = ws.cell(2, site_col).value
|
sites = []
|
||||||
if v2:
|
r = 2
|
||||||
site_name = str(v2).strip()
|
while True:
|
||||||
|
v = ws.cell(r, site_col).value
|
||||||
|
if not v or not str(v).strip():
|
||||||
|
break
|
||||||
|
s = str(v).strip()
|
||||||
|
# 已离开元信息区(遇到分组行/列名行/说明文字/数据区层级)→ 停止读取站点
|
||||||
|
if any(k in s for k in ("基础信息", "商品层级", "必填", "spu", "sku")):
|
||||||
|
break
|
||||||
|
sites.append(s)
|
||||||
|
r += 1
|
||||||
|
if sites:
|
||||||
|
site_name = "、".join(sites)
|
||||||
wb.close()
|
wb.close()
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
return True, "" # 读不到站点信息 → 放行(不阻塞)
|
return True, "" # 读不到站点信息 → 放行(不阻塞)
|
||||||
if not site_name:
|
if not site_name:
|
||||||
return True, "" # 模板无站点信息 → 放行
|
return True, "" # 模板无站点信息 → 放行
|
||||||
if any(k in site_name for k in allowed):
|
if any(any(k in s for k in allowed) for s in site_name.split("、")):
|
||||||
return True, site_name
|
return True, site_name
|
||||||
return False, site_name
|
return False, site_name
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user