Compare commits

5 Commits
Author SHA1 Message Date
3218485270 d68cc3b9e3 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)
2026-09-01 17:24:23 +08:00
3218485270 3dc594cf57 v92-v105 多国适配 + 模板导出增强 + 生图可靠性优化
- 新增韩国(KR)适配:countries/pinterest 种子词、K-pop 提示词、UI 国家列表
- 爬虫跨运行持久化已采集 URL(.collected_urls.json),同关键词重复搜索采新图
- 模板导出增强:SPU 商品属性列名前缀剥离匹配、尺码下拉框 INDIRECT 动态引用、
  SPU 字段映射(袖长/门襟/胸垫等)、季节/印花图案女装映射、建议售价统一必填、
  SKU 分类/数量/单位直接填默认值
- 自定义模式:多模态提示词独立(custom_analyze_*)、生图提示词模板可配置
- Pinterest:空选品守卫、连续空分析保护(max_empty_analyze)、flat_prompt 配置覆盖
- 生图可靠性:开始/完成进度日志、异步任务轮询超时 60s→300s
2026-08-31 18:35:51 +08:00
3218485270 71a48e4ed5 v89-v91 模板增强 + 图源映射 + 多模态提示词可配置化
- 图源映射统一:热点采集与 Pinterest 模式均走 config.product.mark_dirs 配置,按任务序号随机抽模特图/平铺图
- 商品产地固定:统一为「中国大陆」+「产地省份=广东省」(不再读站点/字典映射)
- 模板 SKU 字段检测:按建议售价同一套路检测 SKU分类/SKU数量/SKU数量单位,必填时填入单品/1/件
- 多模态分析提示词可配置:prompts/pinterest_analyze_system.md + user.md,支持国家覆盖,不丢文件回退内置
- 自定义图片模式:新增 pinterest_custom_load_node,图片数量硬校验,选品清单 ≤ 有效图片数
- 模板导出优化:写入前按货号末 3 位升序排序,不再产生空白 xlsx
- 修复 v90 project review 10 项(503 致命终止、线程安全、原子写入等)
2026-08-28 16:24:50 +08:00
3218485270 2a96ec0870 v88 功能增强:产品落盘持久化 + 生图网关适配 + 模板导出优化
- 产品持久化:每完成一个产品立即追加写入 products_pending.jsonl,崩溃不丢已完成产品,finish 读盘合并后统一写模板
- 503 致命错误提前终止:compose/product/seed_shot 端到端识别,提前终止搜索分析,丢弃未完成简报,保留已完成落盘产品直接合成模板
- 模特分配:material_library 合格模特图按任务序号独立随机,同 SPU 多款不再共用同一模特
- 图像网关适配:execution_mode/background 默认不再传入 yunfei 等标准网关,base_url 需带 /v1;429/5xx/空响应退避重试
- Pinterest 分析:删除 term 注入与纯文本降级,失败直接放弃;图片上传前 PIL 完整性校验;suitable_for_print=False 过滤丢弃
- 模板导出:不再产生空白 xlsx,文件名=模板原文件名_已填写;写入前按货号末 3 位升序排序
- 删除对接文档.md,更新 README,gitignore 排除测试产物
2026-08-28 10:28:35 +08:00
3218485270 685b7b0862 更新 README + 模板导出新增面料弹性/沙特阿拉伯码识别
1) README 重写:双模式架构(热点采集/Pinterest 参考模式)、模特性别分组、生图尺寸配置、模板导出增强、一键打包脚本
2) template_export:识别「面料弹性」列填 spu.fabric;识别「尺码表-沙特阿拉伯码」列填 sku.size(均模糊匹配,检测到才填)
2026-08-26 18:32:33 +08:00
75 changed files with 3967 additions and 1067 deletions
+7
View File
@@ -18,9 +18,16 @@ logs/
# 缓存(可重新采集/生成) # 缓存(可重新采集/生成)
.cache/ .cache/
# db 备份(临时备份,不入库)
db/*.bak*
# Pinterest 登录态(首次手动登录后缓存,不入库) # Pinterest 登录态(首次手动登录后缓存,不入库)
pinterest_scraper/.chrome_session/ pinterest_scraper/.chrome_session/
# 测试产物
_probe_out/
test_yunfei_concurrency.py
# 系统 # 系统
.DS_Store .DS_Store
Thumbs.db Thumbs.db
+312 -65
View File
@@ -1,76 +1,332 @@
# POD 热点抓取 AgentLangGraph 工程化版) # POD 热点抓取 AgentLangGraph 工程化版)
自动抓取海外各国(US / GB / JP / AU)实时热点 + 风格趋势,过滤侵权风险,产出可直接用于 自动抓取海外各国(US / GB / JP / AU / MX / DE / BR / SA / PL / ES / IT / CA / KR)实时热点 + 风格趋势,
AI 生图的印花设计提示词。本版本用 **LangGraph** 重写,强调**工程化、节点兜底、可插拔**。 过滤侵权风险,产出可直接用于 AI 生图的印花设计提示词,并完成 **设计稿 → 三合一模特试穿 → OSS 图床 →
种草图 → 商品上传模板** 的完整 POD 生产链路。本版本用 **LangGraph** 重写,强调**工程化、节点兜底、可插拔**。
## 两种运行模式
| 模式 | 入口 | 数据来源 | 适用 |
|---|---|---|---|
| **热点采集模式** | UI「热点采集」 | Google Trendstrending/style/related | 抓国家实时热点词,LLM 筛出可印花设计 |
| **Pinterest 参考模式** | UI「Pinterest 参考模式」 | Pinterest 爬图 + LLM 多模态分析 | 从真实图片提炼原创印花设计,侵权风险更低 |
## 架构:LangGraph 状态图 ## 架构:LangGraph 状态图
### 模式 A:热点采集模式
``` ```
START → seed → fetch → filter → score → screen → prompt_build → compose → END START → seed → fetch → filter → score → screen → prompt_build → compose → product
→ oss_upload → seed_shot → template_export → END
``` ```
| 节点 | 职责 | 可插拔点 / 兜底 | | 节点 | 职责 | 可插拔点 / 兜底 |
|---|---|---| |---|---|---|
| `seed` | 动态生成种子词(trending 派生 + 历史热点 + 月份/节日 → LLM) | 策略可插拔(`graph/seeds/`);LLM 失败回退静态种子 | | `seed` | 动态生成种子词(trending 派生 + 历史热点 + 月份/节日 → LLM) | 策略可插拔(`graph/seeds/`);LLM 失败回退静态种子 |
| `fetch` | 抓取热点(多数据源汇总) | 数据源可插拔(`graph/sources/`);单源失败不影响其它源 | | `fetch` | 抓取热点(Google Trends 多数据源汇总) | 数据源可插拔(`graph/sources/`);单源失败不影响其它源 |
| `filter` | 黑名单 + 真实人物 + 设计相关性三级过滤 | 各级独立 try,单级失败不影响其它级 | | `filter` | 黑名单 + 真实人物 + 设计相关性三级过滤 | 各级独立 try,单级失败不影响其它级 |
| `score` | 归一化 + 跨源融合 + 综合分 | 纯逻辑,空数据返回空 | | `score` | 归一化 + 跨源融合 + 综合分 | 纯逻辑,空数据返回空 |
| `screen` | LLM 合规筛查 + 结构化四要素 | LLM 后端可插拔(`graph/llms/`);调用失败降级 Mock | | `screen` | LLM 合规筛查 + 结构化四要素 | LLM 后端可插拔(`graph/llms/`);调用失败降级 Mock |
| `prompt_build` | 用固定模板装配 image/wearable/composite 提示词 | 四要素缺失时动态推导兜底 | | `prompt_build` | 用固定模板装配 image/wearable/composite 提示词 | 四要素缺失时动态推导兜底 |
| `compose` | `output/<country>/` 产物;可选印图 | 图像后端可插拔(`graph/backends/`);无底图只导出 | | `compose` | 生成纯印花设计稿(白底,可直接打印) | 图像后端可插拔(`graph/backends/`);无底图只导出 |
| `product` | SPU/颜色选品 → 底图 → 三图合成(图1模特+图2设计稿+图3底图) | 缺底图/模特图自动跳过 |
| `oss_upload` | 压缩(3:4 / ≥1340×1785 / <2MB+ 上传阿里云 OSS | 可关闭(`oss.enabled=false` |
| `seed_shot` | 从三合一主图按颜色选取种草图 + 上传 | 按 `seed_shot.count` 随机选取 |
| `template_export` | 把 SPU/SKU 数据填入商品上传模板 | 模板列名模糊匹配,多语言变体兼容 |
### 模式 BPinterest 参考模式
```
START → pinterest_init → pinterest_search → pinterest_scrape → pinterest_analyze
→ [pinterest_route 循环:简报不足 → 回 pinterest_search]
→ pinterest_finalize(排空简报池,后台并发生成 设计→三合一→OSS→种草图)
→ template_export → END
```
| 节点 | 职责 |
|---|---|
| `pinterest_init` | 创建简报池 + 并发生成流水线(`graph/pinterest_pipeline.py` |
| `pinterest_search` | 按需生成搜索词(direct=种子词直接拼「 t-shirt design」;llm=LLM 按需生成,json_schema+防重复) |
| `pinterest_scrape` | 用 Playwright 爬取 Pinterest 图片(共享 Chrome 登录态,并发=1) |
| `pinterest_analyze` | 从图池取图 → 多并发 LLM 多模态分析 → 原创设计简报(侵权检测 + 原创化引导) |
| `pinterest_route` | 简报不足 → 图池有未消费图 → 继续分析;图池不足 → 新一轮搜索;轮次耗尽 → 结束 |
| `pinterest_finalize` | 排空简报池,后台线程池并发生成(设计→三合一→OSS→种草图),合并产品 |
**图池机制**:爬取的图片注册到 `image_pool.json`(路径+md5+搜索词);分析过的图片 md5 一律拉黑
`used_images.json`,合适/不合适都拉黑);图池有未消费图片 → 直接分析(不搜索);不足 → 新一轮搜索。
每个简报带 `image_index`/`source_md5` 全局 id 校验,保证简报、设计稿、三合一套图对应同一张图。
分析前对图片做文件完整性校验(`PIL.verify()` 过滤损坏/截断图),避免把坏 base64 发给多模态网关
导致 400 拒收;分析失败(无有效图片/网关 400)时直接放弃该产品、跳过后续流程(**不做**纯文本降级、
**不做** mock 兜底),由路由自动取新图继续。
**并发控制**`config.yaml``pinterest:`):`analyze_concurrency`(分析线程数)、`analyze_batch`
(每轮取图数,0=自动补齐让 pipeline 队列满并发)、`max_designs`(每搜索词简报上限)、
`scrape_concurrency`(爬图并发,必须=1)、`err400_limit`400 超限放弃当前种子词)、
`max_empty_analyze`(连续空分析轮次上限,0=自动)。
**每个节点都用 `graph/validate.with_fallback` 包裹**:任何未预料异常都被捕获、记入 **每个节点都用 `graph/validate.with_fallback` 包裹**:任何未预料异常都被捕获、记入
`state['errors']`、返回最小更新,整图继续往下走,单点故障不中断流水线。 `state['errors']`、返回最小更新,整图继续往下走,单点故障不中断流水线。致命图像服务错误(503 /
"No available compatible accounts")除外——`with_fallback` 不再静默吞掉,会同步标记流水线终止,
让路由短路到 `pinterest_finalize → template_export` 提前收尾合成模板。
### 模式 B-2:自定义图片模式(Custom)
无需采集/搜索图片,把用户本地图片文件夹里的**有效图片**直接送进多模态分析,沿用 Pinterest
后续所有步骤(分析 → 设计稿 → 三合一 → OSS → 种草图 → 模板导出)。
```
START → pinterest_init → pinterest_custom_load → pinterest_analyze
→ [pinterest_custom_route 循环:图池有未消费图 → 继续分析;图池空/简报达标 → 结束]
→ pinterest_finalize → template_export → END
```
| 节点 | 职责 |
|---|---|
| `pinterest_custom_load` | 读取 `pinterest.custom_image_dir` 文件夹 → 统计有效图片 → **数量硬校验** → 每次重建图池 + 清空已消费拉黑 |
**启用方式**(两种任选):
- 配置 `config.yaml``pinterest.mode: "custom"` 并填 `pinterest.custom_image_dir: "<文件夹路径>"`
- 或直接在 UI 选择「**自定义图片**」流程 →「📁 选图片文件夹…」选择文件夹后运行(自动写入配置)。
**数量硬校验(必过)**
- 必须先选定上传的文件夹(UI 未选会弹窗报错,任务不启动);
- 文件夹内所有有效图片(`jpg/jpeg/png/webp`,递归)数量必须 `> 0`
- **选品清单总数(`product.spu_tasks` 展开后)不得大于有效图片数**。不满足则在
UI 弹窗 / `run_pinterest_ref` 提前抛错 / `custom_load` 节点记录错误,三重拦截,任务不进入分析。
**行为差异**:自定义模式每次运行把图池重建为该文件夹的图片集(唯一图源)并清空已消费 md5 拉黑,
保证用户每次重新上传/选择的所有有效图片都会被重新多模态分析;分析批上限按「选品清单总数」截断,
避免对多余图片浪费配额。`pinterest_custom_route` 只在 wait / analyze / done 间流转,从不 search。
**自定义模式提示词独立**
- **多模态分析提示词**`prompts/custom_analyze_system.md` / `prompts/custom_analyze_user.md`
(与 Pinterest 模式的 `pinterest_analyze_*` 完全独立,互不影响);
- **生图提示词模板**`config.yaml``custom.prompt_pieces``image_prompt_template` /
`negative_prompt`)可编辑——以参考图片的整体氛围/主题/时代/风格类型为灵感,**明确禁止复制或模仿**
参考图片的印刷 artwork、角色、布局、文字及服装/模特等元素,仅基于观察到的主题/技术/调色板/布局
生成新设计;留空回退代码内置 `CUSTOM_IMAGE_PROMPT_TEMPLATE` / `CUSTOM_NEGATIVE_PROMPT`
## 目录结构 ## 目录结构
``` ```
pod_trend_agent/ pod_trend_agent/
├── cli.py # 命令行入口(替代旧 run/screen/compose ├── cli.py # 命令行入口
├── config.yaml # 全局配置(数据源、权重、黑名单、LLM、模板、compose ├── ui_app.py # 桌面 UITkinter
├── configs/countries/ # 各国专属数据源种子(US|GB|JP|AU.yaml ├── config.yaml # 全局配置(数据源、权重、黑名单、LLM、模板、compose/product/oss/seed_shot
├── configs/
│ ├── countries/ # 各国 Google Trends 种子词(US|GB|JP|AU|MX|DE|BR|SA|PL|ES|IT|CA|KR.yaml
│ ├── pinterest/ # 各国 Pinterest 种子词池(<CC>.yaml
│ ├── model_features.yaml # 模特特征库(按性别分组 female/male,种草图用)
│ ├── kids_features.yaml # ★ 童装特征库(男童/女童:模特、场景、服装风格,种草图用)
│ ├── seed_shot_templates.yaml # 种草图提示词模板
│ └── style_features.yaml
├── prompts/<country>/ # ★ 每个国家不同的提示词单独文件夹 ├── prompts/<country>/ # ★ 每个国家不同的提示词单独文件夹
│ ├── system_prompt.md # 该国 LLM 系统提示(补充段,叠加到默认规则) │ ├── system_prompt.md # 该国 LLM 系统提示(补充段,叠加到默认规则)
│ └── aesthetics.yaml # 审美 hint + 风格-配色 extra 规则 + 额外黑名单 │ └── aesthetics.yaml # 审美 hint + 风格-配色 extra 规则 + 额外黑名单
├── prompts/pinterest_analyze_system.md # 多模态分析系统提示词(Pinterest/自定义模式),可配置
├── prompts/pinterest_analyze_user.md # 多模态分析用户提示词,可配置
├── prompts/custom_analyze_system.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 兜底 + 数据校验
│ ├── loader.py # 配置/提示词加载与合并 │ ├── loader.py # 配置/提示词加载与合并
│ ├── scoring.py # 归一化/融合/过滤(纯逻辑) │ ├── scoring.py # 归一化/融合/过滤(纯逻辑)
│ ├── style_rules.py # 动态风格/配色推导 │ ├── style_rules.py # 动态风格/配色推导
│ ├── templates.py # 固定提示词模板(按国家区分,30×38cm 上限,文字敏感规则) │ ├── templates.py # 固定提示词模板(按国家区分,文字敏感规则)
│ ├── classify.py # 热点分类 │ ├── classify.py # 热点分类
│ ├── sources/ # 数据源可插拔(google_trends / pinterest + 注册表) │ ├── pinterest.py # Pinterest 图池/黑名单/压缩/MD5 去重
│ ├── pinterest_pipeline.py # 简报池 + 并发生成流水线(Pinterest 模式核心)
│ ├── product.py / product_batch.py # 三合一合成 / 批量
│ ├── seed_shot.py # 种草图选取 + 模特特征(性别分组)
│ ├── template_export.py # 商品上传模板导出(列名模糊匹配)
│ ├── oss_upload.py # 图片压缩 + 阿里云 OSS 上传
│ ├── sources/ # 数据源可插拔(google_trends + 注册表)
│ ├── llms/ # LLM 后端可插拔(mock / openai_compat + 注册表) │ ├── llms/ # LLM 后端可插拔(mock / openai_compat + 注册表)
│ ├── backends/ # 图像后端可插拔(openai) │ ├── backends/ # 图像后端可插拔(openai / mock
│ ├── seeds/ # 种子词策略可插拔(static / dynamic + holidays 月份节日 │ ├── seeds/ # 种子词策略可插拔(static / dynamic + holidays
│ ├── nodes/ # 7 个流水线节点(seed │ ├── nodes/ # 流水线节点(seed/fetch/.../pinterest_*/product/oss/seed_shot/template_export
│ └── agent.py # 构建并编译 StateGraphrun_country() │ └── agent.py # 构建并编译 StateGraphbuild_graph / build_pinterest_graph
├── templates/
│ └── template_router.py # 商品上传模板路由(列名定位、数据写入)
├── scripts/
│ └── package.py # 一键打包脚本(PyInstaller → dist_vXX
├── basemap/ # 平铺衣服底图(<款号>/<SKU.code>/
├── material_library/ # 模特图(<品类>/
├── db/spu_sku.db # SPU/SKU 数据库
├── pinterest_scraper/ # Playwright 爬虫 + Chrome 登录态(含跨运行持久化 .collected_urls.json
└── output/<country>/ # ★ 每个国家的产物独立文件夹 └── output/<country>/ # ★ 每个国家的产物独立文件夹
├── design_briefs.json # 含 国家/热点词/类别/风险/设计稿提示词/完整印图提示词 ├── design_briefs.json/md # 设计简报(含 image_prompt / composite_prompt
├── design_briefs.md ├── designs/ # 纯印花设计稿
├── composite_prompts.json/md # 封装提示词包(印到底图用) ├── product/ # 三合一合成图 + 已填写商品模板 <SKU>_已填写.xlsx
── report.md # 各阶段统计 + 兜底错误记录 ── Preview/ # ★ 成功导入模板产品的 _composite.oss.jpg 压缩图 + result.xlsx(货号|中文标题|英文标题)
└── report.md # 各阶段统计 + 兜底错误记录
``` ```
## 可插拔指南 ## Pinterest 参考模式详解
- **加数据源**:在 `graph/sources/` 新建类继承 `DataSource` 实现 `fetch()`,在 1. **种子词池**`configs/pinterest/<CC>.yaml` 存各国风格化种子词(含节日词),偏向印花设计。
`graph/sources/__init__.py``SOURCES` 登记;在 `config.yaml``sources:` 加入。 2. **按需搜索**:每次只生成 1 个搜索词(`search_terms_per_run=1`),自动追加「 t-shirt design」后缀
- **加 LLM 后端**:在 `graph/llms/` 新建类继承 `LLMBackend` 实现 `screen()`,在 (保证爬到的图是真实印花艺术,而非生活方式/风景图);爬取成功才标记已用,失败不拉黑。
`LLM_BACKENDS` 登记;`llm_screen.provider` 选择(openai/deepseek/qwen/moonshot 走 openai_compat)。 3. **爬图**Playwright 打开 Pinterest 搜索页截图采集(共享 `.chrome_session` 登录态,并发强制=1)。
- **改种子词策略**`seed_provider``static`(仅 yaml/ `mock`(规则生成,零成本)/ `openai_compat`(真 LLM)。 每个关键词维护**跨运行持久化已采集 URL 集合**`save_dir/.collected_urls.json`):滚动采集时跳过
动态策略收集 trending 派生 + 上轮 safe 历史热点 + 月份/临近节日上下文,由 LLM 后端生成种子词, 历史 URL 旧图(不计入 count),继续下拉直至凑满新图或滚动到底(连续 10 轮无新图停止)——同一关键词
与 yaml 静态种子合并后注入 `style.seeds` / `related.seed_keywords``config.yaml``seed_provider_cfg` 限量) 重复搜索也能采到新图,而非永远返回顶部旧图
- **加国家**:建 `configs/countries/<CODE>.yaml`(种子词)与 `prompts/<CODE>/`(提示词), 4. **图池 + MD5 去重**:图片注册 `image_pool.json`;分析过的 md5 一律拉黑(`used_images.json`);
`config.yaml``countries:` 加入即可,无需改代码 设计稿生成后全局 MD5 去重(`.cache/global_design_md5.json`),跨国家生效
- **加图像后端**:在 `graph/backends/` 实现 `ImageBackend``compose.backend` 配置后 5. **多模态分析**:大图先压缩(max 1024px / 1.5MB)再送 LLM,产出原创设计简报(motif/风格/配色/构图 +
`cli.py -c <国家> --base 底图.png` 印图 risk_levelsafe/review/blocked)。blocked 拦截、review 加原创化魔改引导、safe 直接生成
每张图由多模态模型判定 `suitable_for_print`(是否适合做印花),False 的简报在节点层被过滤丢弃,
且该图 md5 立即拉黑,不进入生成流程。
6. **并发生成**:简报推入 `PinterestPipeline`,后台线程池(`product.concurrency`,默认 5)逐条
生成 设计稿 → 三合一 → OSS 上传 → 种草图,边分析边生成,不等全部分析完。
每完成一个产品立即追加落盘到 `output/<country>/<ts>/products_pending.jsonl`(中断/崩溃也不丢已
完成产品),全部完成后统一合并写模板。
7. **空选品守卫**:任务清单(`self._tasks`)为空时直接弹窗拦截并提示「请先添加选品(款号 → 颜色 →
+添加选品)」;`run_pinterest_pipeline` 也有空选品守卫,即使绕过 UI 直接调用也会拒绝启动。
8. **连续空分析保护**:图片分析连续多轮无新增简报(分析失败 / 图片均不适合印花)时,达到
`pinterest.max_empty_analyze` 上限(0=自动:目标 SPU 数+2,至少 3)即终止分析直接收尾,
避免「逐一取下一张参考图」式无限空转。
## 多模态分析提示词(可配置)
Pinterest 参考模式与自定义图片模式的多模态分析提示词已**外部化为配置**,无需改代码即可调优:
- `prompts/pinterest_analyze_system.md`——多模态分析**系统提示词**(POD T 恤设计分析师角色 +
禁止抄袭/违禁项/输出 JSON 结构等规则);
- `prompts/pinterest_analyze_user.md`——**用户提示词**"Analyze the attached image and produce
one ORIGINAL T-shirt print design brief...")。
**加载优先级**`graph/llms/openai_compat_backend.py`):
1. `prompts/<国家>/pinterest_analyze_*.md`(国家专属覆盖);
2. `prompts/pinterest_analyze_*.md`(全局配置,先查运行根 exe 旁再查内置);
3. 内置默认常量(兜底,向前兼容)。
直接编辑这两份文件即可自定义分析与生图引导规则;`pinterest_analyze_system.md` 里可调整
`image_prompt` 二段式结构 / `suitable_for_print` 判定标准等约束(多模态分析不再产出
`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"**:端到端识别
(设计稿 compose → 三合一 product → 种草图 seed_shot),置位终止标志后**提前终止任务**——
停止继续搜索/分析,`abort_unfinished` 丢弃未完成简报,**已完成落盘的产品保留**,直接合成模板导出。
- **图像 API 重试退避**`graph/backends/openai_image_backend.py`):429 尊重网关 `Retry-After`
头,否则指数退避;5xx500/502/503/504 超时)与网关空响应(JSON 解析失败)均退避 3/6/9s 重试、
最多 3 次,避免空响应/超时风暴触发限流。
- **生图进度日志**:每次生图调用提交前打印 `[img] 开始图生图/文生图(模型,尺寸,参考图 N 张)→ 文件名`
保存后打印 `[img] 已生成(耗时 Xs`——同步成功不再静默,UI 日志区能实时看到生图进度,避免"看起来卡死"。
- **异步任务轮询超时**`_wait_task` 轮询超时 60s → 300s(与提交请求 timeout 一致)。网关异步任务实际
需 30s~6 分钟(设计稿/三图合成),60s 轮询超时会把仍在处理中的任务误判为失败而重复提交,白白消耗配额和时间。
- **yunfei / 标准 OpenAI 网关适配**`compose.execution_mode``background` 参数默认不再传入——
yunfei 等网关不认识它们会返回空 body;`base_url` 需带 `/v1` 前缀(如
`https://img.yunfei.best/v1`),否则请求错误路径得到空响应。
- **图源映射(可配置)**`config.product.mark_dirs` 定义 SPU.mark → `material_library` 下的
`model_dir`(模特图)/ `flat_dir`(平铺图)两个图源文件夹及对应合成提示词。热点采集与 Pinterest
两种模式统一走该映射:在「有图的文件夹」间按**任务序号**独立随机抽图,抽到模特图用模特三图提示词
(图1模特+图2印花+图3底图),抽到平铺图用平铺三图提示词(图1平铺实拍+图2印花+图3底图)。
同一 SPU 的多款不再共用同一张图;某类目录无图则只用另一类。每张种草图随机取不同模特特征。
**合成提示词可配置覆盖**`mark_dirs` 下配置 `flat_prompt` / `model_prompt` 时优先生效
`product_node._active_prompt`),留空才回退内置 `FLAT_LAY_PROMPT` / `MODEL_WEAR_PROMPT`
## 模特性别分组
`configs/model_features.yaml` 的模特特征按性别分组(`female` / `male`)。种草图生成时读取商品上传
模板的「类目」表头值:
- 类目含「男童」→ 固定从 **boy_kids** 组随机取模特(童装)
- 类目含「女童」→ 固定从 **girl_kids** 组随机取模特(童装)
- 类目含「男」(如 `男士时尚>男装>男装T恤`)→ 固定从 **male** 组随机取模特
- 类目含「女」→ 固定从 **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,不硬编码)
| 图片类型 | 配置项 | 默认 |
|---|---|---|
| 纯印花设计稿 | `compose.design_size` | 1024x10241:1 |
| 三合一合成图 | `compose.size` | 1536x20483:4 |
| 种草图 | `seed_shot.size` | 1536x20483:4 |
## 商品上传模板导出
生成产品图后,从 db 读该 SPU/SKU 信息(材质/成分/图案/领型/面料/克重/印花类型/尺码表/边长/包装重量…),
`templates/template_router.py` 的路由函数填入商品上传模板,输出 `{模板原文件名}_已填写.xlsx`
`output/<国家>/product/`。模板已自包含在项目 `templates/``config.product.template_path`,可改为自由上传)。
- 数据从模板第 6 行(数据区空行)开始填,SPU 与各尺码 SKU 紧邻;
- **写入前按货号最后 3 位升序排序**:`template_export` 在批量填模板前取货号(`img_code`,前端
`code_prefix` 前缀命名的货号)末 3 位数字从小到大排序再顺序插入,保证 SPU/SKU 块按货号有序,
不受后台并发生成顺序影响;
- **不再产生空白 xlsx**:修复了旧版本探测模板可写性时误建空文件的 bug,product 目录只保留填好
的有效文件,文件名 = 模板原文件名 + `_已填写`(非固定「商品上传」);
- **列名模糊匹配**:模板列名按名称匹配(非固定行列),支持中/英/日变体;
- **胸围识别**:识别「基码表-胸围(cm)」「胸围全围(cm)」等含「胸围」的列,全部填 `sku.bust` 值;
- **申报价格**:模糊匹配所有含「申报价格」的列,统一按加价后价格填写(`price × (1+markup_percent%)`);
- **款式来源**:SPU 行「款式来源」统一填「现货款」;
- **商品产地**:所有国家统一填「中国大陆」;新增「产地省份」精确匹配列,统一填「广东省」(不再读站点/做字典映射);
- **SKU 分类 / SKU 数量 / SKU 数量单位**:模板检测到这些列即统一填固定值(不再检测「非必填」备注):
`SKU分类=单品``SKU数量=1``SKU数量单位=件`
- **建议售价 / 建议售价单位**:模板存在对应列且 `suggested_price_ratio > 0` 时统一必填——
建议售价 = 申报价格 ×(1+比例%),单位统一填 `CNY`(不再检测模板备注);
- **SPU 商品属性列名兼容**`resolve_col` 支持「SPU商品属性-袖型」前缀剥离匹配——列名带前缀的模板
(如沙特女装模板)精确匹配,列名不带前缀的模板(如黑白CasualNest Shop)自动剥离前缀后匹配;
- **尺码下拉框动态引用**`_read_size_options` 识别 `INDIRECT(...)` 动态引用公式,能解析则按下拉选项
过滤 SKU 行(尺码不在下拉内跳过),无法解析(空单元格/跨表)时返回 None 不启用过滤,避免误杀全部 SKU 行;
- **SPU 商品属性字段映射**`placket_type→门襟类型``sleeve_type→袖型``sleeve_length_type→袖长`
`breast_pad→胸垫``layout→版型``silhouette→廓形``length_type→长度``belt→腰带`
`occasion→场合``scene→场景``hemline_shape→下摆形状`,各字段独立不覆盖;
- **季节映射(女装)**`spu.season` 的「四季」→「ALL/全球/所有」(`config.yaml``season_map.female` 可配置);
- **印花图案映射(女装)**`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);
- 规格类型2 = `{size}*{color}`;币种=CNY;发货仓1~N 取模板顶头按「、」分隔,库存均 200;
- 模板文件被占用(打开中)时自动换名 `<SKU>_已填写_N.xlsx`,不中断导出;
- **多颜色 + 模式**`sku_code` 支持逗号分隔多颜色;`spu_per_color`true=每颜色一个 SPU 块 /
false=单 SPU 挂多颜色变体);CLI 用 `--single-spu` 切单 SPU 模式,UI 用「颜色多选 + 模板模式」。
## 用法 ## 用法
```bash ```bash
# 跑全部国家(默认 US GB JP AU) # 跑全部国家(热点采集模式,默认 US GB JP AU
python cli.py python cli.py
# 只跑英国,Mock 兜底 LLM # 只跑英国,Mock 兜底 LLM
@@ -81,6 +337,13 @@ python cli.py -c US
# 提供平铺衣服底图,把设计印上去(需 config.compose.backend + api_key # 提供平铺衣服底图,把设计印上去(需 config.compose.backend + api_key
python cli.py -c US --base path/to/flatlay.png python cli.py -c US --base path/to/flatlay.png
# 查看 db 可选款号 / 某款颜色
python cli.py --list-spus
python cli.py --list-colors DG004
# 指定款号+颜色跑流水线(product.backend=mock 占位 / openai 真生图需 compose.api_key
python cli.py -c GB --spu DG004 --sku DG004-BL01
``` ```
## 桌面 UITkinter 轻量版) ## 桌面 UITkinter 轻量版)
@@ -91,56 +354,40 @@ python ui_app.py
# 无 GUI 自检(验证环境/核心链路,结果写入运行根 self_test_result.txt # 无 GUI 自检(验证环境/核心链路,结果写入运行根 self_test_result.txt
python ui_app.py --self-test python ui_app.py --self-test
# 打包 exe(单文件、无控制台;数据文件打进 _MEIPASSoutput/.cache 写在 exe 旁)
.venv/Scripts/pyinstaller -F -w --name PODTrendAgent \
--add-data "config.yaml;." --add-data "configs;configs" --add-data "prompts;prompts" \
--hidden-import pytrends --hidden-import pytrends.request ui_app.py
# 产物:dist/PODTrendAgent.exe
``` ```
UI 功能:国家多选、LLM 后端选择(mock/static/openai_compat...)、种子上限、**SPU/颜色选品**、运行(后台线程 + 实时日志) UI 功能:国家多选、**流程选择(热点采集 / Pinterest 参考模式 / 自定义图片模式)**、LLM 后端选择、种子上限
结果表格(国家/热点/类别/风险/分数,双击看完整提示词)、打开产物目录。 **SPU/颜色选品**、运行(后台线程 + 实时日志)、结果表格(双击看完整提示词)、打开产物目录。
打包后首次运行自动在 exe 旁创建 `output/``.cache/`,并生成一份**可编辑的默认配置**
`config.yaml` / `configs/` / `prompts/`);exe 运行时**优先读 exe 旁这份配置**(改了就生效),
exe 内置的作为兜底默认。想恢复默认:删掉 exe 旁对应文件即可。
## 产品图生成(product ## 打包
热点提示词 → SPU/颜色选品 → basemap 底图 → 印花图 → 模特试穿合成图,产物在 `output/<country>/product/`
```bash ```bash
# 查看 db 可选款号 / 某款颜色 # 一键打包(自动取下一个版本号)
python cli.py --list-spus python scripts/package.py
python cli.py --list-colors DG004
# 指定款号+颜色跑流水线(product.backend=mock 占位 / openai 真生图需 compose.api_key # 指定版本号
python cli.py -c GB --spu DG004 --sku DG004-BL01 python scripts/package.py 84
``` ```
- 数据关系:`SPU.code`=款号,`SKU.code`=`款号-颜色编码`(如 DG004-BL01);底图在 `basemap/<款号>/<SKU.code>/`,模特图在 `material_library/<品类>/` 产物:`dist_vXX/PODTrendAgent.exe`。打包流程:PyInstaller 构建(不跑自检)→ 同步数据目录
- 缺底图/模特图时对应步骤自动跳过并提示;`product.backend` 留空则仅存底图。 config.yaml / configs / prompts / templates / basemap / material_library / db / output / logs)→
- **商品上传模板自动导出**:生成产品图后,从 db 读该 SPU/SKU 信息(材质/成分/图案/领型/面料/克重/印花类型/尺码表/边长/包装重量…),经 `templates/template_router.py` 的路由函数填入商品上传模板, 复制当前 Chrome 登录态到 exe 同目录(`pinterest_scraper/.chrome_session`)→ 同步上一个版本的 `.cache`
输出 `<SKU>_已填写.xlsx``output/<国家>/product/`。模板已自包含在项目 `templates/``config.product.template_path`,可改为自由上传) exe 运行时优先读 exe 旁这份配置(改了就生效),exe 内置的作为兜底默认
- 数据从模板第 6 行(数据区空行)开始填,SPU 与各尺码 SKU 紧邻;
- SKC货号:SPU 行=SPU.code、SKU 行=SKU.codeSKU货号暂不填;
- 规格类型2 = `{size}*{color}`;币种=CNY;发货仓1~N 取模板顶头按「、」分隔,库存均 200;
- 商品轮播图列名中/英/日变体均可模糊匹配;SPU 行轮播图1=生成首图,SKU 行轮播图2~5=db `img_url_2~5`(无则回退生成图);
- 模板文件被占用(打开中)时自动换名 `<SKU>_已填写_N.xlsx`,不中断导出;
- **多颜色 + 模式**`sku_code` 支持逗号分隔多颜色(如 `DG015-VT01,DG015-DARK HEATHER`);`spu_per_color`true=每颜色一个 SPU 块 / false=单 SPU 挂多颜色变体);CLI 用 `--single-spu` 切单 SPU 模式,UI 用「颜色多选 + 模板模式」。
- UI 打开时**默认加载勾选国家的热点缓存**(`output/<国家>/design_briefs.json`)直接展示,点「运行」刷新最新。
## 数据流要点 ## 数据流要点
0. `seed`(动态种子词):收集 trending 派生(去噪 + 人名 + 风险护栏过滤)、上轮 safe 历史热点、 0. `seed`(动态种子词):收集 trending 派生(去噪 + 人名 + 风险护栏过滤)、上轮 safe 历史热点、
当前月份/临近节日,由 LLM 后端(mock 规则 / 真 LLM)生成动态种子词,与 yaml 静态种子合并注入抓取配置。 当前月份/临近节日,由 LLM 后端(mock 规则 / 真 LLM)生成动态种子词,与 yaml 静态种子合并注入抓取配置。
1. `fetch``gt_trending`(国家 RSS 实时榜)+ `gt_style`/`gt_related`(风格/行业种子关联词,种子来自 seed 节点)。 1. `fetch``gt_trending`(国家 RSS 实时榜)+ `gt_style`/`gt_related`(风格/行业种子关联词)。
2. `filter` 三级过滤:合规黑名单(全局 + 各国 `extra_blacklist`)→ 真实人物 → 泛新闻/科技/赛事词。 2. `filter` 三级过滤:合规黑名单(全局 + 各国 `extra_blacklist`)→ 真实人物 → 泛新闻/科技/赛事词。
3. `score` 按 (source,kind) 分组 min-max 归一化、跨源融合加权重。 3. `score` 按 (source,kind) 分组 min-max 归一化、跨源融合加权重。
4. `screen` LLM 合规筛查,产出 risk_level / 四要素;`keep_review=false` 时丢弃待复核。 4. `screen` LLM 合规筛查,产出 risk_level / 四要素;`keep_review=false` 时丢弃待复核。
5. `prompt_build` 用四要素 + 固定模板装配 image/wearable/composite 提示词(结构一致、可复用 img2img)。 5. `prompt_build` 用四要素 + 固定模板装配 image/wearable/composite 提示词(结构一致、可复用 img2img)。
6. `product`(可选)SPU/颜色选品 → basemap 底图 → 印花图 → 模特试穿合成(`output/<country>/product/` 6. `compose` 生成纯印花设计稿(白底,可直接打印),product 取第一张作为图2 复用
7. `compose` 写各国 `output/<country>/`;给底图则调图像后端印图 7. `product` SPU/颜色选品 → basemap 底图 → 三图合成(图1模特 + 图2设计稿 + 图3底图)→ 模板
8. `oss_upload` 压缩(3:4 / ≥1340×1785 / <2MB+ 上传阿里云 OSSURL 写回 `*_url` 字段。
9. `seed_shot` 从三合一主图按颜色选取种草图(模特性别分组),上传 OSS,URL 写入模板「详情图文」。
10. `template_export` 把 SPU/SKU 数据填入商品上传模板,输出 `<SKU>_已填写.xlsx`
> 合规红线(最重要):版权/商标/真实人物/敏感内容一律拦截或降级;二创/重绘仍有风险, > 合规红线(最重要):版权/商标/真实人物/敏感内容一律拦截或降级;二创/重绘仍有风险,
> 拿不准进"待复核"。各国侵权盲区不同(如英国 WWE/游戏/乐队、日本动漫 IP),放在 > 拿不准进"待复核"。各国侵权盲区不同(如英国 WWE/游戏/乐队、日本动漫 IP),放在
Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

+176 -8
View File
@@ -14,6 +14,7 @@ countries:
- ES - ES
- IT - IT
- CA - CA
- KR
# —— 可插拔数据源 —— # —— 可插拔数据源 ——
# 在 graph/sources/ 注册表里登记;这里列出要启用的。 # 在 graph/sources/ 注册表里登记;这里列出要启用的。
@@ -39,22 +40,83 @@ seed_provider_cfg:
# 按需搜索:每次只生成 1 个搜索词,用完(爬取成功)才标记已用;简报不足时循环再搜,直到满足 SPU 数量。 # 按需搜索:每次只生成 1 个搜索词,用完(爬取成功)才标记已用;简报不足时循环再搜,直到满足 SPU 数量。
pinterest: pinterest:
enabled: true enabled: true
mode: "scrape" # Pinterest 模式:scrape=在线爬图引用(原流程);custom=自定义本地图片文件夹直接送多模态分析
# custom 模式:无需采集/搜索,把 custom_image_dir 文件夹内所有有效图片直接进多模态分析,沿用 Pinterest 后续所有步骤
custom_image_dir: "" # 自定义模式图片文件夹路径(必须填写;文件夹内所有有效图片 jpg/jpeg/png/webp 都会进入分析)
# 数量校验:有效图片数必须 ≥ 选品清单(spu_tasks 展开后)总数,否则任务不启动
provider: openai # 搜索词/图片分析用 LLM 提供商(openai / mock provider: openai # 搜索词/图片分析用 LLM 提供商(openai / mock
search_mode: direct # 搜索词生成方式:direct=跳过LLM,直接搜「种子词 t-shirt design」;llm=LLM按需生成搜索词 search_mode: direct # 搜索词生成方式:direct=跳过LLM,直接搜「种子词+后缀」;llm=LLM按需生成搜索词
search_term_suffix: " t-shirt design" # Pinterest 搜索词自动追加的自定义后缀(可编辑;留空=不追加),保证 Pinterest 返回真正的印花图
search_terms_per_run: 1 # 每次搜索词数量(direct 模式=从种子池随机取 N 个直接拼后缀;llm 模式=每次生成 1 个) search_terms_per_run: 1 # 每次搜索词数量(direct 模式=从种子池随机取 N 个直接拼后缀;llm 模式=每次生成 1 个)
max_search_rounds: 0 # 搜索轮次上限(0=自动:目标 SPU 数×2,至少 5;防网络故障无限循环) max_search_rounds: 0 # 搜索轮次上限(0=自动:目标 SPU 数×2,至少 5;防网络故障无限循环)
max_empty_analyze: 0 # 连续图片分析无新增简报的轮次上限(0=自动:目标SPU数+2,至少3;防"逐一取下一张参考图"空转)
seed_sample: 40 # 每次从国家种子池随机抽取多少个种子词给 LLM seed_sample: 40 # 每次从国家种子池随机抽取多少个种子词给 LLM
max_used_terms_in_prompt: 100 # 已用搜索词最多注入 LLM 提示词的个数(防 token 超限) max_used_terms_in_prompt: 100 # 已用搜索词最多注入 LLM 提示词的个数(防 token 超限)
images_per_term: 40 # 每个搜索词爬取图片数量 images_per_term: 40 # 每个搜索词爬取图片数量
analyze_per_term: 1 # 每次调用分析 1 张图(一张对应一个设计,一次 API 请求;简报带 image_index 全局 id 校验防错位) analyze_per_term: 1 # 固定每次调用分析 1 张图(一次 API 请求一张,利于 LLM 注意力;简报带 brief_id uuid4 全局校验防错位)
analyze_concurrency: 6 # 并发分析线程数(多并发提取简报) analyze_concurrency: 6 # 并发分析线程数(多并发提取简报)
analyze_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次补齐到目标所需/每词简报上限,让 pipeline 队列时刻满并发 analyze_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次最多分析 max_designs 张;搜集完一批→生成一批简报,不够用了才主动采集
max_designs: 10 # 每个搜索词最多生成多少个设计简报 max_designs: 10 # 每个搜索词最多生成多少个设计简报
ref_images_per_design: 1 # 生图时每个设计附带几张爬取图作为参考(发给生图模型) ref_images_per_design: 1 # 生图时每个设计附带几张爬取图作为参考(发给生图模型)
supply_attempts: 3 # 设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数 supply_attempts: 3 # 设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数
err400_limit: 15 # 每个种子词累计 400(且错误含「内容/图片」)超限后放弃该种子词(多模态+生图合计) err400_limit: 15 # 每个种子词累计 400(且错误含「内容/图片」)超限后放弃该种子词(多模态+生图合计)
scrape_concurrency: 1 # 同时爬取几个搜索词(共享 .chrome_session 登录态,Chrome 单例,必须=1) scrape_concurrency: 1 # 同时爬取几个搜索词(共享 .chrome_session 登录态,Chrome 单例,必须=1)
headless: false # 爬取时是否无头(false=显示 Chrome 窗口,首次需手动登录) headless: false # 爬取时是否无头(false=显示 Chrome 窗口,首次需手动登录)
proxy: "" # 图片下载代理(空=自动探测系统代理/VPN,默认走代理;配了如 http://127.0.0.1:7890 则用指定代理)
login_check: true # 爬取前静态检测 Pinterest 登录态(只读 .chrome_session cookies,不启动 Chrome;未登录则跳过本轮爬取并告警)
login_wait: true # 运行时检测到未登录时:false=跳过并告警(不阻塞);true=弹出 Chrome 等待手动登录
# —— Pinterest 模式生图提示词的固定段(可编辑;留空=回退代码内置默认)——
# 拼装顺序:image_prompt + print_suffix + spelling_rule(仅含文字时)
# 凡 review 简报额外追加 review_rebrand_hint。
prompt_pieces:
print_suffix: >-
standalone pure print design on a pure white background, the print artwork is SMALL and
CENTERED with clearly larger white margins around it, print area between about 15x18 cm and
26x32 cm, do NOT fill the entire canvas, do NOT force full-bleed, do NOT add any gradient,
texture or background color behind the artwork, no garment, no shirt, no model, no mannequin,
no watermark
spelling_rule: >-
Render every phrase shown in quotes exactly as written, correctly spelled.
review_rebrand_hint: >-
IMPORTANT: this theme is ONLY a loose stylistic reference. Do NOT reproduce any brand logo,
trademark, character, mascot, copyrighted artwork or real person. Create a fully ORIGINAL
design with a different name and distinct visual details and colors — a generic, non-infringing
homage in the same mood, clearly distinct from the original.
# 自定义模式(pinterest.mode=custom)专属配置:多模态提示词与生图固定段独立于 Pinterest 参考模式。
custom:
# 自定义模式生图提示词的固定段(可编辑;留空=回退代码内置默认)。
# 自定义模式生图正向提示词 = image_prompt_template{image_prompt} 替换为分析模型产出的新设计描述);
# 负向 = negative_promptcompose 生图时作为负向参数传给图像后端)。
# 凡 review 简报额外追加 review_rebrand_hint。
prompt_pieces:
image_prompt_template: >-
Use the attached bestseller product photo only as loose inspiration for
overall mood, theme, era and style genre — do NOT reproduce, trace,
rearrange, recolor or closely imitate its printed artwork, characters,
layout or text, and do NOT render a shirt, garment, model, hanger, photo
scene, product mockup or background of any kind. First, carefully examine
the printed artwork on the garment in the attached photo: zoom in mentally
on the print area, identify its subject, technique, palette and layout,
and base the new design on THOSE observed traits. Ignore the model,
background and photo quality. Then: {image_prompt}
negative_prompt: >-
copy of reference artwork, lookalike of the bestseller print, characters,
mascots, likenesses, logos, trademarks, watermark, photorealistic shirt,
apparel, product mockup, model, garment, hanger, busy background
print_suffix: >-
standalone pure print design on a pure white background, the print artwork is SMALL and
CENTERED with clearly larger white margins around it, print area between about 15x18 cm and
26x32 cm, do NOT fill the entire canvas, do NOT force full-bleed, do NOT add any gradient,
texture or background color behind the artwork, no garment, no shirt, no model, no mannequin,
no watermark
spelling_rule: >-
Render every phrase shown in quotes exactly as written, correctly spelled.
review_rebrand_hint: >-
IMPORTANT: this theme is ONLY a loose stylistic reference. Do NOT reproduce any brand logo,
trademark, character, mascot, copyrighted artwork or real person. Create a fully ORIGINAL
design with a different name and distinct visual details and colors — a generic, non-infringing
homage in the same mood, clearly distinct from the original.
# 跨源融合权重(按 source 标签,无需和为 1) # 跨源融合权重(按 source 标签,无需和为 1)
# 已下调 gt_trending(泛国家热点只作微弱信号),主力偏向 style+related(可印花型词)。 # 已下调 gt_trending(泛国家热点只作微弱信号),主力偏向 style+related(可印花型词)。
@@ -64,6 +126,81 @@ weights:
gt_related: 0.30 gt_related: 0.30
pinterest: 0.0 pinterest: 0.0
# —— 成分映射表(可配置,仅作用于 component_1/2/3,绝不作用于 material)——
# 女装模板下拉框是「中文+英文」格式,db 存中文,需映射到下拉选项;找不到映射保留原值。
# 男装模板保留 db 原值(不查此表,不在此配置)。key=db 成分值,value=模板下拉框精确选项(一个字都不能错)。
component_map:
female:
: 棉Cotton
聚酯纤维: 聚酯纤维(涤纶)Polyester
涤纶: 聚酯纤维(涤纶)Polyester
再生聚酯纤维: 聚酯纤维(涤纶)Polyester
聚酯纤维(涤纶): 聚酯纤维(涤纶)Polyester
氨纶: 氨纶Elastane
锦纶: 锦纶(尼龙)Polyamide
尼龙: 尼龙Nylon
腈纶: 腈纶Acrylic
蚕丝: 蚕丝Silk
亚麻: 亚麻Linen
莫代尔: 莫代尔Modal
粘纤: 粘胶(人棉)Viscose
粘胶: 粘胶(人棉)Viscose
粘胶纤维: 粘胶(人棉)Viscose
人棉: 人棉Rayon
碳纤维: 碳纤维Carbon
金属: 金属Metal
羊毛: 羊毛Wool
羊绒: 羊绒Cashmere
马海毛: 马海毛Mohair
兔毛: 兔毛Rabbit Hair
牛皮革: 牛皮革Bovine Leather
羊皮革: 羊皮革Sheep Leather
猪皮革: 猪皮革Pig Leather
PU: PU Polyurethane
PVC: PVC Polyvinyl Chloride
铜氨纤维: 铜氨纤维Cupro
莱赛尔: 莱赛尔(天丝)Lyocell
天丝: 莱赛尔(天丝)Lyocell
醋酸纤维: 醋酸纤维Acetate
醋酯纤维: 醋酸纤维Acetate
羽绒: 羽绒Down
羽毛: 羽毛Feather
羊驼毛: 羊驼毛Alpaca
苎麻: 苎麻
竹纤维: 竹纤维
三醋酯纤维: 三醋酯纤维Triacetate
金属镀膜纤维(金银丝): 金属镀膜纤维(金银丝)
聚丙烯纤维: 聚丙烯纤维
其他纤维: 其他纤维
# 季节值映射表(女装模板专用):key=db 季节值,value=模板下拉框精确选项。
# 男装模板保留 db 原值(不查此表,不在此配置)。缺失/留空回退内置默认(四季→ALL/全球/所有)。
season_map:
female:
四季: ALL/全球/所有
# 印花图案值映射表(女装模板专用):key=db pattern 值,value=模板下拉框精确选项。
# 男装模板保留 db 原值(不查此表,不在此配置)。缺失/留空回退内置默认(印花→卡通)。
pattern_map:
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
@@ -155,6 +292,8 @@ RULES:/n1.底色锁定:从图3提取衣服底色与面料,最终合成中必须1
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, IT-market aesthetic: renaissance elegance, roman heritage, tuscan countryside warmth, la dolce vita retro charm, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or an Italian slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names" image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, IT-market aesthetic: renaissance elegance, roman heritage, tuscan countryside warmth, la dolce vita retro charm, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or an Italian slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
CA: CA:
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, CA-market aesthetic: maple-leaf pride, northern-lights wonder, rocky-mountain outdoors, cozy cottage and hockey spirit, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a French slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names" image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, CA-market aesthetic: maple-leaf pride, northern-lights wonder, rocky-mountain outdoors, cozy cottage and hockey spirit, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a French slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
KR:
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, KR-market aesthetic: trendy K-pop street culture, Seoul neon nightlife, Y2K retro-futurism, cute-hip aesthetic, bold typography with hangul accents, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Korean slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
# —— 第三阶段 compose:生成纯印花设计稿 + 导出简报包(在 product 之前)—— # —— 第三阶段 compose:生成纯印花设计稿 + 导出简报包(在 product 之前)——
# 用前 N 个 safe 简报的 image_prompt 调图像后端文生图,产物 output/<country>/designs/ # 用前 N 个 safe 简报的 image_prompt 调图像后端文生图,产物 output/<country>/designs/
@@ -162,12 +301,12 @@ RULES:/n1.底色锁定:从图3提取衣服底色与面料,最终合成中必须1
# backend 留空则不生成设计稿(product 节点会自行回退生成)。 # backend 留空则不生成设计稿(product 节点会自行回退生成)。
compose: compose:
backend: "openai" # 生成设计稿的图像后端:openai(需 api_key/ mock(占位);留空=不生成 backend: "openai" # 生成设计稿的图像后端:openai(需 api_key/ mock(占位);留空=不生成
api_key: "sk-ef1f251f059f9c2f8cc397de4a84e96d65775e1f2d32bb13b69b23dafc24d111" api_key: "sk-1CicJFACR4JCeLe6LTdsqSd0ffSrybMgmdfRiNfIlKok8MbS"
base_url: "https://api.tofastcode.xyz" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404 base_url: "https://img.yunfei.best/v1" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404
model: "gpt-image-2" model: "gpt-image-2"
size: "1536x2048" # 合成图/种草图等非设计图的统一尺寸(3:4) size: "1536x2048" # 合成图/种草图等非设计图的统一尺寸(3:4)
design_size: "1024x1024" # 纯印花设计稿尺寸(1:1 design_size: "1024x1024" # 纯印花设计稿尺寸(1:1
background: "transparent" # 生成透明背景 PNG(gpt-image 系列支持);留空=默认背景 execution_mode: "" # 网关异步/同步模式参数(仅 ai-media 等网关需要,如 "sync");yunfei 等标准 OpenAI 网关不认识该参数,留空=不传
seed: 0 # 随机种子:>0=固定(相同 seed 可复现,需网关支持);0/留空=每次随机 seed: 0 # 随机种子:>0=固定(相同 seed 可复现,需网关支持);0/留空=每次随机
design_count: 1 # 用前 N 个 safe 简报生成设计稿(product 取第一个复用) design_count: 1 # 用前 N 个 safe 简报生成设计稿(product 取第一个复用)
design_workers: 5 # 设计稿并发生成线程数(仅热点采集模式 compose 节点用;Pinterest 模式走 pipeline 总并发 product.concurrency design_workers: 5 # 设计稿并发生成线程数(仅热点采集模式 compose 节点用;Pinterest 模式走 pipeline 总并发 product.concurrency
@@ -190,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底图)→ 模板 ——
@@ -202,16 +342,44 @@ product:
db_path: "db/spu_sku.db" db_path: "db/spu_sku.db"
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 下的图文件夹。
# 每个 mark 可配置两个图源(模特图文件夹 / 平铺图文件夹),运行时在「有图的文件夹」间随机抽图:
# - 抽到 model_dir(模特图)→ 用模特三图提示词(图1模特+图2印花+图3底图)
# - 抽到 flat_dir(平铺图) → 用平铺三图提示词(图1平铺实拍+图2印花+图3底图)
# 三图合成提示词模板已迁移到独立 md 文件(不再在此配置):
# prompts/model_prompt.md 模特三图提示词;prompts/flat_prompt.md 平铺三图提示词
# 读取优先级:运行根(exe 旁)prompts/ > 数据根 prompts/;文件缺失/留空回退代码内置默认。
# 该映射完全可配置、不写死在代码里。
mark_dirs:
"1":
model_dir: "模特图" # 模特图文件夹名(material_library 下)
flat_dir: "平铺图" # 平铺图文件夹名(material_library 下)
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"
spu_per_color: # 留空自动判定:单色=每色一SPU;多色=单SPU多色(UI 不选择模板模式);CLI --single-spu 可显式覆盖 spu_per_color: # 留空自动判定:单色=每色一SPU;多色=单SPU多色(UI 不选择模板模式);CLI --single-spu 可显式覆盖
markup_percent: 0 # 加价百分比(按 SKU 最低价,供后续定价使用) markup_percent: 0 # 加价百分比(按 SKU 最低价,供后续定价使用)
suggested_price_ratio: 30 # 建议售价比例(%):建议售价 = 申报价格 × (1+此比例/100);模板「建议售价」必填时才填
code_prefix: "DG" # 货号前缀(OSS 上传 key:{国家}/{时间戳}/{前缀+3位计数}_{4位随机}.jpg000 起最多 999 code_prefix: "DG" # 货号前缀(OSS 上传 key:{国家}/{时间戳}/{前缀+3位计数}_{4位随机}.jpg000 起最多 999
backend: "mock" # openai / mock / 留空=跳过生成 backend: "mock" # openai / mock / 留空=跳过生成
background: "transparent" # 设计稿透明背景(product 后端生图时也传 background=transparent
template_dir: "templates" # template_router.py 所在目录(项目内自包含) template_dir: "templates" # template_router.py 所在目录(项目内自包含)
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"
+94
View File
@@ -0,0 +1,94 @@
# 韩国专属数据源配置(configs/countries/KR.yaml
trending:
enabled: true
limit: 40
style:
enabled: true
seeds:
- kpop
- seoul
- korean aesthetic
- seoul nightlife
- seoul neon
- y2k
- korean street fashion
- k-streetwear
- korean drama aesthetic
- korean cafe
- hanbok
- hanok
- gyeongbokgung
- korean traditional pattern
- dancheong
- korean calligraphy
- hangul
- korean mask dance
- korean drum
- korean fan dance
- taekwondo
- korean tiger
- kumiho
- korean fox
- korean crane
- korean magpie
- korean zodiac
- cherry blossom korea
- jinhae cherry blossom
- korean autumn leaves
- ginkgo
- maple korea
- korean pine
- mugunghwa
- korean flower
- plum blossom korea
- kimchi
- bibimbap
- tteokbokki
- kimbap
- bulgogi
- samgyeopsal
- korean fried chicken
- korean street food
- bingsu
- hotteok
- korean dessert
- jeju island
- hallasan
- jeju coast
- busan
- haeundae
- hangang
- namsan tower
- seoul skyline
- korean retro
- korean vintage
- seoul art
- korean minimal
- korean wildlife
- korean mountain
- korean sea
- korean fishing village
- korean lantern
- korean festival
related:
enabled: true
seed_keywords:
- korean retro
- seoul street art
- korean vintage
- kpop aesthetic
- hanbok pattern
- korean traditional
- korean cafe art
- y2k korea
- korean wildlife
- korean minimal
- seoul art
- korean food
- korean pattern
- korean summer
- korean music
- jeju nature
- korean folk art
- seoul night
timeframe: today 3-m
+111
View File
@@ -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:
- "童趣甜美风,奶呼呼的可爱、柔和治愈,适合小动物与卡通图案短袖"
- "元气活泼风,明亮干净、元气满满,适合水果与星星图案短袖"
- "热带水果乐园风,色彩缤纷、甜蜜多汁,适合西瓜菠萝印花短袖"
- "清新度假风,阳光灿烂、夏日沙滩感,适合海洋元素与彩色字母短袖"
- "奶油温柔风,温暖治愈、软糯高级,适合纯色与小刺绣短袖"
+57 -44
View File
@@ -1,75 +1,88 @@
# 模特特征库 —— 跨境电商基础款服装展示专用(已剔除外部穿搭与夸张妆造干扰 # 模特特征库 —— 跨境电商年轻款服装展示专用(全库年龄20-33岁
# 占位符:[模特特征] 随机取用 # 占位符:[模特特征] 随机取用
# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装。 # 取用规则:
# 按性别分组:female=女模 / male=男模 # 1. 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机
# 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机。 # 2. 如需同一商品多图保持同一模特(一景多动作),首次随机取到后请固定复用,不要每张图重新随机。
# 3. 站点适配:北美/欧洲站优先「欧美系+多元族裔系」,东南亚站优先「东南亚裔」,日韩站优先「亚洲系」。
# 描述规范:每条 = 年龄 + 族裔/风格标签 + 发型发色 + 肤色肤质 + 气质表情(字段顺序统一,出图更稳定)。
# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装,不描述下半身。全库限定年轻人群。
model_features: model_features:
female: female:
# ========== 女模 - 亚洲/清新/通勤类(适合基础款、纯色T恤 ========== # ========== 1. 亚洲系(适合基础款/日韩风/通勤 ==========
- "22岁亚洲清新女学生,通透伪素颜,黑直发,清瘦骨感" - "22岁亚洲清新系女模,黑长直发,通透伪素颜,清瘦骨感,邻家亲和感"
- "25岁日韩系通勤女模,元气通透底妆,棕色微卷发,亲切感" - "25岁日韩系通勤女模,棕色微卷发,元气通透底妆,亲切微笑感"
- "27岁亚洲极简风女模,冷白皮,无瑕光泽肌,利落黑色短发" - "27岁亚洲极简风女模,利落黑色短发,冷白皮,无瑕光泽肌,知性气质"
- "29岁亚洲轻熟女模,骨相美,微雾感底妆,气质温婉" - "29岁亚洲轻熟女模,微卷深棕发,微雾感底妆,骨相精致,温婉气质"
- "31岁亚洲职场御姐女模,中分黑长发,干净底妆,干练自信气场"
# ========== 女模 - 欧美/阳光/甜心类(适合印花T恤度假风) ========== # ========== 2. 欧美阳光(适合印花T恤/度假风) ==========
- "20岁欧美甜心女模,金发碧眼,阳光加州感,健康小麦色肌肤" - "20岁欧美甜心女模,金色长直发,碧眼,健康小麦色肌肤,阳光加州感笑容"
- "23岁法式慵懒女模,法式刘海微卷棕发,通透红唇" - "23岁法式慵懒女模,法式刘海微卷棕发,通透底妆,柔和红唇,松弛气质"
- "27岁欧美自然系女模,淡雀斑,大地色淡妆,健康好气色" # 新增 - "27岁欧美自然系女模,浅棕长发,淡雀斑,大地色淡妆,健康好气色"
- "21岁迈阿密热辣女孩,金棕色大波浪,高光立体修容,健康古铜色肌肤" - "21岁迈阿密热辣女孩,金棕色大波浪,健康古铜色肌肤,立体修容,明媚笑容"
- "24岁北美学院风女模,高马尾,通透元气妆,白皙肌肤" - "24岁北美学院风女模,高马尾,白皙肌肤,通透元气妆,青春活力感"
- "34岁北美宝妈系女模,自然淡妆,栗色长发,亲切温暖的微笑" # 新增 - "26岁北美邻家系女模,栗色长发,自然淡妆,亲切温暖笑容"
# ========== 女模 - 欧美/街头/个性类(适合街头风、美式复古印花) ========== # ========== 3. 欧美街头系(适合美式复古/街头印花) ==========
- "22岁纽约下城区酷女孩,挑染发色,清冷厌世感,苍白质肌肤" - "22岁纽约下城区酷女孩,灰紫色挑染发,苍白冷感肌肤,酷感眼神"
- "28岁欧美高级中性模,凌乱短发,冷白皮,骨感清冷" - "28岁欧美高级中性风女模,凌乱金色短发,冷白皮,骨感清冷高级脸"
- "26岁拉美混血风女模,深邃眼窝,野生挑眉,健康小麦色肌肤" - "26岁拉美混血风女模,深棕长卷发,深邃眼窝,健康小麦色肌肤,野性眉形"
- "25岁Y2K千禧辣妹,银色挑染发,清透裸妆,微胖立体感" - "25岁Y2K千禧辣妹,银色挑染发,清透裸妆,微胖立体感,个性张扬"
# ========== 女模 - 极简/性冷淡/高街(适合纯色基础款卫衣) ========== # ========== 4. 极简高街(适合纯色基础款/卫衣) ==========
- "30岁北欧极简风女模,冷白皮,无瑕哑光肌,银色利落短发" - "30岁北欧极简风女模,银色利落短发,冷白皮,无瑕哑光肌,疏离高级感"
- "29岁纽约高街潮流女模,冷感通透妆,深棕色直发,疏离眼神" - "29岁纽约高街潮流女模,深棕色直发,冷感通透妆,清冷眼神"
- "31岁哥本哈根极简风设计师女模,利落黑色短发,黑白灰冷感气质" - "31岁哥本哈根极简风设计师女模,利落黑色短发,黑白灰冷感气质"
- "28岁法式先锋艺术系女模,单边耳坠,冷感素颜,深金色发" - "28岁法式先锋艺术系女模,深金色长发,冷感素颜,单边耳坠,艺术家气质"
# ========== 女模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备 ========== # 新增分组 # ========== 5. 多元族裔系(北美/欧洲站覆盖 ==========
- "26岁非裔自然卷女模,健康深色肌肤,光泽感底妆,明快笑容" - "26岁非裔自然卷女模,健康深色肌肤,光泽感底妆,明快笑容"
- "29岁非裔都市丽人女模,利落编发,精致底妆,自信干练气场" - "29岁非裔都市丽人女模,利落编发,精致底妆,自信干练气场"
- "24岁印度裔女模,浓密黑长发,深邃大眼,暖调蜜糖色肌肤" - "24岁印度裔女模,浓密黑长发,深邃大眼,暖调蜜糖色肌肤"
- "27岁拉丁裔阳光女模,栗色卷发,蜜色肌肤,热情爽朗笑容" - "27岁拉丁裔阳光女模,栗色卷发,蜜色肌肤,热情爽朗笑容"
- "32岁中东裔优雅女模,微卷黑发,轮廓深邃,端庄知性气质" - "32岁中东裔优雅女模,微卷黑发,轮廓深邃,端庄知性气质"
- "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容" # 大码:覆盖plus size市场 - "25岁东南亚裔清甜女模,黑长直发,蜜糖色肌肤,元气亲和笑容"
# ========== 6. 大码系(Plus Size市场) ==========
- "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容"
- "30岁欧美大码甜心女模,金色微卷发,健康白皙肌肤,自信明媚笑容"
male: male:
# ========== 男模 - 亚洲/盐系/清新类(适合基础款日韩 ========== # ========== 1. 亚洲系(适合基础款/日韩 ==========
- "23岁日系盐系男模,单眼皮,清瘦骨感,柔软黑发" - "23岁日系盐系男模,柔软黑发,单眼皮,清瘦骨感,干净少年感"
- "25岁亚洲清冷系男模,冷白皮,碎发自感,下颌线清晰" - "25岁亚洲清冷系男模,碎发自感,冷白皮,下颌线清晰,清冷气质"
- "28岁亚洲轻熟男模,干净利落的短发,健康肤色,稳重气质" - "28岁亚洲轻熟男模,干净利落的短发,健康肤色,稳重气质"
- "21岁亚洲阳光男大,通透底妆,微卷黑发,清爽笑容" - "21岁亚洲阳光系男模,微卷黑发,通透底妆,清爽笑容"
- "26岁亚洲运动系男模,利落黑色短发,健康小麦色肌肤,阳光笑容"
# ========== 男模 - 欧美/街头/运动(适合美式复古街头印花) ========== # ========== 2. 欧美街头/运动(适合美式复古/街头印花) ==========
- "20岁加州冲浪系男模,阳光金发,麦色健康肌,清爽笑容" - "20岁加州冲浪系男模,阳光金发,麦色健康肌,清爽笑容"
- "24岁欧美健身教练型男,肌肉线条分明,小麦色肌肤,硬朗骨相" - "24岁欧美健身教练型男,肌肉线条分明,小麦色肌肤,硬朗骨相"
- "22岁纽约街头滑板男模,脏辫,清冷厌世感,苍白肌肤" - "22岁纽约街头滑板男模,深色脏辫,苍白肌肤,酷感眼神"
- "27岁美式机车硬汉男模,粗犷廓,络腮胡,硬挺气质" - "27岁美式机车男模,粗犷廓,络腮胡,年轻硬朗气质"
- "35岁北美成熟奶爸系男模,浅胡茬,栗色短发,温暖亲切笑容" # 新增 - "26岁北美暖男系男模,浅胡茬,栗色短发,温暖亲切笑容"
# ========== 男模 - 欧美/商务/雅痞(适合重磅纯色T恤卫衣) ========== # ========== 3. 商务/雅痞(适合重磅纯色T恤/卫衣) ==========
- "25岁英伦雅痞型男,复古油头,短胡茬,熟男气质" - "25岁英伦雅痞型男,复古油头,短胡茬,痞帅气质"
- "30岁华尔街商务精英男模,背头,沉稳内敛气质,干净皮肤" - "30岁华尔街商务精英男模,背头,干净皮肤,沉稳内敛气质"
- "28岁意大利型男,深邃五官,意式凌乱卷发,健康肌肤" - "28岁意大利型男,意式凌乱卷发,深邃五官,健康肌肤"
- "32岁法式儒雅大叔,微卷发,金丝眼镜,温润如玉气质" - "26岁法式文艺男模,微卷发,金丝眼镜,温柔书卷气"
# ========== 男模 - 极简/暗黑/高街(适合纯色基础款暗黑风) ========== # ========== 4. 极简高街(适合纯色基础款/暗黑风) ==========
- "25岁欧美高冷男模,银白发色,苍白冷白皮,锋利眼神" - "25岁欧美高冷男模,银白发色,苍白冷白皮,锋利眼神"
- "29岁柏林暗黑系青年,苍白皮肤,冷感眼神,黑色直发" - "29岁柏林暗黑系青年,黑色直发,苍白皮肤,冷感眼神"
- "26岁东欧赛博风男模,高颧骨,清冷骨相,浅色唇" - "26岁东欧赛博风男模,高颧骨,清冷骨相,浅色唇"
- "33岁北欧性冷淡风男艺术家,银色短发,疏离眼神,极简气质" - "33岁北欧性冷淡风男艺术家,银色短发,疏离眼神,极简气质"
# ========== 男模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备 ========== # 新增分组 # ========== 5. 多元族裔系(北美/欧洲站覆盖 ==========
- "27岁非裔阳光型男模,健康深色肌肤,利落寸头,爽朗笑容" - "27岁非裔阳光型男模,健康深色肌肤,利落寸头,爽朗笑容"
- "31岁非裔商务型男模,修剪整齐的短须,挺拔身姿,可靠稳重气场" - "31岁非裔商务型男模,修剪整齐的短须,挺拔身姿,可靠稳重气场"
- "26岁拉丁裔热情男模,微卷黑发,蜜色肌肤,明亮笑容" - "26岁拉丁裔热情男模,微卷黑发,蜜色肌肤,明亮笑容"
- "29岁印度裔斯文男模,黑短发,浓眉,温和儒雅气质" - "29岁印度裔斯文男模,黑短发,浓眉,温和儒雅气质"
- "30岁中东裔深邃男模,黑色微卷发,轮廓立体,沉稳内敛" - "30岁中东裔深邃男模,黑色微卷发,轮廓立体,沉稳内敛"
- "33岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容" # 大码:覆盖plus size市场 - "25岁东南亚裔阳光男模,黑色短发,蜜色肌肤,清爽亲和笑容"
# ========== 6. 大码系(Plus Size市场) ==========
- "29岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容"
- "27岁欧美大码阳光男模,浅络腮胡,魁梧体型,憨厚亲切笑容"
+89
View File
@@ -0,0 +1,89 @@
# Pinterest 参考模式种子词(面向视觉灵感,非热点关键词)
# 约束:避开品牌/角色/名人/宗教/国旗/酒精等侵权与敏感项
seeds:
- 케이팝 일러스트
- 서울 네온 야경
- 레트로 한국
- Y2K 디자인
- 한복 패턴
- 단청 문양
- 한옥 일러스트
- 경복궁 그림
- 한글 타이포그래피
- 한국 전통문양
- 매화 그림
- 벚꽃 일러스트
- 단풍 일러스트
- 은행잎 패턴
- 호랑이 그림
- 구미호 일러스트
- 한국 까치
- 두루미 그림
- 한국 토끼
- 강아지 일러스트
- 고양이 일러스트
- 귀여운 동물
- 별자리 일러스트
- 달 그림
- 별 그림
- 고래 일러스트
- 돌고래 그림
- 물고기 패턴
- 나비 일러스트
- 잠자리 그림
- 무당벌레
- 다람쥐 일러스트
- 펭귄 그림
- 팥빙수 일러스트
- 떡볶이 일러스트
- 김밥 그림
- 비빔밥 일러스트
- 불고기 그림
- 치킨 일러스트
- 라면 그림
- 만두 일러스트
- 커피 일러스트
- 카페 디자인
- 과일 일러스트
- 딸기 그림
- 수박 패턴
- 감귤 그림
- 복숭아 일러스트
- 제주도 풍경
- 부산 해운대
- 한강 야경
- 남산타워 일러스트
- 서울 스카이라인
- 한국 스트릿 패션
- 한국 카페 감성
- 레트로 한국 포스터
- 한국 미니멀
- 한국 자연
- 한국 바다
- 태권도 일러스트
- 한국 탈춤
- 사물놀이 일러스트
- 한국 전통 악기
- 한국 춤
- 코스모스 그림
- 진달래 일러스트
- 개나리 그림
- 국화 일러스트
- 소나무 그림
- 대나무 일러스트
- 연꽃 그림
- 국화 패턴
- 물고기 비늘 패턴
- 소라 그림
- 조개 일러스트
- # —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
- 설날 디자인
- 추석 일러스트
- 보름달 그림
- 크리스마스 디자인
- 할로윈 일러스트
- 발렌타인 디자인
- 어린이날 일러스트
- 스승의날 그림
- 한복 입은 캐릭터
- 전통놀이 일러스트
+79 -74
View File
@@ -1,174 +1,179 @@
# 种草图提示词模板库 —— 跨境电商详情页专用版(稳定出图+零遮挡强约束+得体姿态+全亮调场景版) # 种草图提示词模板库 —— 跨境电商上衣专用版(服装主体构图+印花中心+零遮挡强约束+得体姿态+全亮调场景+结构部件保色+必穿下装版)
# 占位符:[商品名称]、[材质]、[模特特征]、[服装风格] # 占位符:[商品名称]、[材质]、[模特特征]、[服装风格]
# 摄影参数说明:35mm/50mm镜头控制自然透视,f/1.4-f/2.8控制景深虚化,8k强化面料细节,3:4符合电商标准长图比例。 # 核心逻辑:画面主体永远是衣服。取景自头顶留白至下摆以下,整件衣服100%完整入画,
# 头部占比≤1/4且五官弱化,视觉中心恒定为胸前印花。
# 下装规则:模特必须真实穿着下装(长裤/牛仔裤等),下装仅是不入画,绝不等于不穿;画面下沿严禁出现裸腿或内裤。
# 结构保色规则:全部模板统一内置一句通用约束(仅负面禁止,无商品类型条件判断)——
# 抽绳/帽子/罗纹等结构部件禁止被改色、禁止被画上印花颜色,短袖无此部件自动不生效,卫衣自动锁色。
# 摄影参数说明:50mm为主力镜头控制自然透视,f/1.4-f/2.8控制景深虚化,8k强化面料细节,3:4符合电商标准长图比例。
seed_shot_templates: seed_shot_templates:
# ========================================== # ==========================================
# 一、 高转化白底/灰底主图区(聚焦正面印花) # 一、 高转化白底/灰底主图区(聚焦正面印花)
# ========================================== # ==========================================
- name: "纯白背景正面展示" - name: "纯白背景正面服装主体展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,身体正对镜头微侧15度,眼神直视镜头,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部位于画面最上沿且占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔自然,肩部放松下沉,双手自然垂放在身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白无缝背景纸前,身体正对镜头微侧15度,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
- name: "高级浅灰背景展示" - name: "高级浅灰背景服装主体展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔自然,肩部放松,双手自然垂或单手插兜姿态收尾于身体两侧,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。
- name: "纯白背景双手展示下摆" - name: "纯白背景下摆与版型展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手背身后或自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景半身构图,模特站在纯白背景纸前,眼神直视镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,重点同时覆盖胸前印花与下摆缝线,二者均须完整清晰;头部占比不超过画面高度的四分之一,五官弱化不做强调,禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,双手背身后或自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,身体正对镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺及宽松版型。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
- name: "低饱和克莱因蓝背景棚拍" - name: "低饱和克莱因蓝背景服装主体棚拍"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特在低饱和克莱因蓝的无缝背景纸前摆出具有张力的站姿,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔有张力,肩线平直,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特在低饱和克莱因蓝的无缝背景纸前,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
- name: "纯白背景微动态走动展示" - name: "纯白背景微转体动态展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,呈现正对镜头的微动态走动状态,整体呈现[服装风格]的调性,展示衣服正面印花在动下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身向一侧微转约30度后回望镜头方向,肩部放松,双手自然垂放于身体两侧,转动幅度不得使印花侧转到画面边缘之外,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白无缝背景纸前,呈现轻微转体的微动态,展示衣服正面印花在动下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
# ========================================== # ==========================================
# 二、 面料与印花特写区(打消购买顾虑) # 二、 面料与印花特写区(打消购买顾虑)
# ========================================== # ==========================================
- name: "纯白背景领口细节特写" - name: "纯白背景领口细节特写"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示衣服的领口、袖口等工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装细节构图:镜头聚焦领口与上胸区域,取景范围为鼻底以下至腰部以上,画面主体是这件[商品名称]的领口罗纹、缝线工艺、肩部做工与上胸区域的印花上缘;模特面部仅保留唇部以下的局部作比例陪衬,禁止以五官为画面焦点,禁止把镜头对准脸部,禁止裁切所要展示的衣服细节部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特肩部放松下沉,双手不在画面内,绝对不可遮挡衣服任何部位。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,画面聚焦展示衣服的领口工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,85mm镜头,f/2.0光圈,8k分辨率。
- name: "明亮室内逆光面料透气质感展示" - name: "明亮室内逆光面料透气质感展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,边缘形成强烈的轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是被逆光照透的面料与胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,在衣服边缘形成柔和轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。
- name: "双手拉扯领口弹性展示" - name: "双手拉扯领口弹性展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捏住领口两侧向外轻拉展示弹力,绝对不可将手放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,50mm镜头,f/2.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装细节构图:镜头聚焦领口与上胸区域,取景范围为鼻底以下至腰部以上,画面主体是双手轻拉领口的拉伸状态、领口罗纹回弹纹理与上胸区域的印花上缘;模特面部仅保留唇部以下的局部作比例陪衬,禁止以五官为画面焦点,禁止把镜头对准脸部,禁止裁切所要展示的衣服细节部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特双手捏住领口左右两侧边缘向外轻拉展示弹力,手部仅接触领口边缘线,绝对不可移向胸口中央遮挡印花。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,85mm镜头,f/2.8光圈,8k分辨率。
- name: "低头凝视正面印花特写" - name: "低头凝视正面印花特写"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特站在纯白背景纸前,微微低头凝视衣服胸前的印花图案,整体呈现[服装风格]的调性,画面以斜俯视角度聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;模特微微低头使视线落在胸前印花上,头部随低头角度自然变小并略带透视缩短,成为氛围陪衬,画面的绝对视觉中心依然是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特微微低头注视胸前印花,头发不得垂落遮盖肩部与印花区域,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡衣服。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,85mm镜头,f/2.0光圈,8k分辨率。
# ========================================== # ==========================================
# 三、 居家生活场景区(营造松弛感与舒适度) # 三、 居家生活场景区(营造松弛感与舒适度)
# ========================================== # ==========================================
- name: "居家沙发坐姿场景" - name: "居家沙发服装主体坐姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手自然放在腿上或沙发扶手上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特得体地坐在明亮现代极简风格的客厅布艺沙发上,身体正对镜头微侧,姿态松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的舒适度。室内采用柔和的高调漫反射自然光,背景为高度虚化的明亮居家环境。画面比例为3:4。柯达Portra 400胶片质感,色彩柔和明亮,35mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特端坐于沙发上,上半身自然挺直微侧,双手自然搭在大腿或沙发扶手上远离胸口,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特得体地坐在明亮现代极简风格的客厅布艺沙发上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的舒适度。室内采用柔和的高调漫反射自然光,背景为高度虚化的明亮居家环境。画面比例为3:4。柯达Portra 400胶片质感,色彩柔和明亮,50mm镜头,f/2.0光圈,8k分辨率。
- name: "居家落地窗自然光展示" - name: "居家落地窗自然光服装主体展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在高层公寓明亮的落地窗前,身体正前方对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在高层公寓明亮的落地窗前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透,衣服受光充足色彩还原准确。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
- name: "周末清晨床头坐姿场景" - name: "周末清晨床头服装主体坐姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手自然放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特得体地坐在床边,展现周末清晨的松弛感,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的亲肤无拘束感。室内采用清晨明亮的高调自然光,背景为高度虚化的卧室家具。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特端坐于床沿,上半身自然挺直,双手自然搭在大腿上远离胸口,被子或床品绝对不可堆叠遮挡衣服下摆。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特得体地坐在明亮的床边,展现周末清晨的松弛感,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的亲肤无拘束感。室内采用清晨明亮的高调自然光,背景为高度虚化的卧室环境。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。
- name: "居家厨房场景" - name: "居家厨房服装主体场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手操作厨具,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特站在现代开放式厨房中岛台,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止围裙等其他服饰遮挡衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特站在中岛台侧面,上半身放松自然,双手轻搭台面边缘远离胸口,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮的现代开放式厨房中岛台,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
- name: "周末居家阅读场景" - name: "周末居家阅读服装主体场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手必须捧书放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特盘腿坐在卧室的羊毛地毯上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料的柔软亲肤感。室内采用柔和的窗光侧逆光,背景为高度虚化的卧室床铺与暖色氛围灯。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止书本遮挡衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特坐在明亮飘窗边,书本置于腰腹高度的画面下沿位置,上胸与胸前印花完全不被遮挡,另一只手自然垂放,绝对不可将书举至胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮的窗边阅读,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料的柔软亲肤感。室内采用柔和的窗光侧顺光保证衣服受光均匀,背景为高度虚化的温馨居家环境。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。
# ========================================== # ==========================================
# 四、 户外休闲/运动场景区(展现百搭与透气) # 四、 户外休闲/运动场景区(展现百搭与透气)
# ========================================== # ==========================================
- name: "阳光草坪户外站姿" - name: "阳光草坪服装主体站姿"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的城市林荫道上,身体正对镜头微侧,微微低头微笑,整体展现[服装风格]的氛围,凸显[材质]在自然行动中的透气与百搭。午后阳光透过树叶洒下明亮光斑,背景为高度虚化的过往行人与都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,35mm镜头,f/1.7大光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止强烈树影大面积压暗衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光充足的城市林荫道上,身体正对镜头微侧,表情放松带浅笑,整体展现[服装风格]的氛围,凸显[材质]在自然光照下的透气与百搭。午后阳光透过树叶洒下明亮光斑,光斑只落在背景上不打在衣服表面,衣服受光均匀色彩准确。背景为高度虚化的都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,50mm镜头,f/1.7大光圈,8k分辨率。
- name: "都市街头OOTD站姿展示" - name: "都市街头OOTD服装主体展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手拿咖啡杯放在腰侧,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的现代商业街区外墙前,身体正对镜头微靠在墙上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身微靠墙面,肩部放松,咖啡杯持有手收在腰侧画面最下沿,另一只手自然垂放,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光明媚的现代商业街区外墙前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,光线均匀打亮衣服正面,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
- name: "天台蓝天白云站姿场景" - name: "天台蓝天白云服装主体场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光,无死角的展现服装颜色。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/2.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光均匀打在衣服上,无死角的还原服装真实颜色。画面比例为3:4。徕卡Q2摄影质感,50mm镜头,f/2.8光圈,8k分辨率。
- name: "自然公园站姿外景" - name: "自然公园服装主体站姿外景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的公园草坪前,微风拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光,画面明亮通透,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止衣物被风吹乱遮盖印花,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光充足的公园绿植前,微风拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光板补光,衣服受光均匀色彩鲜亮,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。
- name: "街头滑板站姿场景" - name: "滑板公园服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚踩在滑板上,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的滑板公园边缘,身体正对镜头,微风吹拂发丝,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮阳光形成侧逆光,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/1.7光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光明媚的滑板公园,身体正对镜头,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮阳光形成轻微侧光并配合反光板给衣服暗部补光,保证印花区域无死黑,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,50mm镜头,f/1.7光圈,8k分辨率。
- name: "林荫道骑行停靠场景" - name: "林荫道骑行服装主体场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚点地双手握把,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特骑着一辆复古自行车停在两旁长满梧桐树的林荫小道上,转头看向镜头微笑,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性。明亮阳光透过树叶洒下光斑,背景为高度虚化的树干与明亮光斑。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特站在复古自行车旁,握车把的手位于画面最下沿腰腹高度,另一只手自然垂放,头微转向镜头方向,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特停在两旁长满梧桐树的林荫小道旁,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性。明亮阳光透过树叶洒下光斑,光斑只点缀背景避开衣服表面,衣服受光均匀,背景为高度虚化的树干与明亮光斑。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。
- name: "海边沙滩站姿场景" - name: "海边沙滩服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在细软的沙滩上,海风吹起衣摆,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,画面明亮高调,背景为高度虚化的蔚蓝海水、白沙滩与远处海平线。画面比例为3:4。索尼A7R4画质,35mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的休闲短裤或长裤并搭配鞋子,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在细软的沙滩上,海风轻扬发丝,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,衣服过曝得到良好控制,色彩鲜明通透,背景为高度虚化的蔚蓝海水与远处海平线。画面比例为3:4。索尼A7R4画质,50mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。
- name: "露营帐篷坐姿场景" - name: "露营帐篷服装主体坐姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手端着搪瓷杯放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的自然光,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,35mm镜头,f/1.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特端坐于露营椅上,上半身自然挺直,搪瓷杯收在腰腹高度的画面下沿,另一只手自然垂放,绝对不可举杯遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的漫射自然光,衣服受光均匀色彩还原准确,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,50mm镜头,f/1.8光圈,8k分辨率。
- name: "浅水沙滩站姿场景" - name: "海边浅滩服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在浅水沙滩边,海风吹起衣摆,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与飘逸感。光线为正午到下午的明亮阳光,海面反光形成自然补光,背景为高度虚化的蔚蓝大海、白沙滩与天空。画面比例为3:4。夏日清透色彩,高饱和度,35mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的休闲短裤或长裤并搭配鞋子,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止海水打湿变色衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在海边浅滩旁,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈感。光线为下午的明亮阳光,海面反光形成天然柔和补光打亮衣服正面,背景为高度虚化的蔚蓝大海与天空。画面比例为3:4。夏日清透色彩,高饱和度,50mm镜头,f/2.0光圈,8k分辨率。
- name: "热带绿植温室站姿场景" - name: "热带绿植温室服装主体场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在长满大型龟背竹与天堂鸟的植物园温室中,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与透气感。光线为穿透玻璃顶的明亮自然光,形成明亮的光斑,背景为高度虚化的茂密绿植。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.4大光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止植物枝叶伸入前景遮挡衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,与身后绿植保持距离,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在长满大型龟背竹与天堂鸟的植物园温室中,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与透气感。光线为穿透玻璃顶的明亮均匀自然光,衣服表面光影干净无杂乱叶影,背景为高度虚化的茂密绿植。画面比例为3:4。富士Provia 400X色彩预设,50mm镜头,f/1.4大光圈,8k分辨率。
- name: "现代美术馆纯白站姿外景" - name: "现代美术馆纯白服装主体外景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代美术馆纯白外墙前,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,55mm镜头,f/2.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特背靠在明亮的现代美术馆纯白外墙前,气质清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,墙面反光为衣服提供均匀补光,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
- name: "山顶站姿场景" - name: "山顶服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在山顶的岩石上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光,画面通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,35mm镜头,f/2.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在山顶的岩石平台上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光并配合天空的自然柔光罩效果,衣服受光均匀色彩通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,50mm镜头,f/2.8光圈,8k分辨率。
# ========================================== # ==========================================
# 五、 工作/通勤场景区(展现日常实用度) # 五、 工作/通勤场景区(展现日常实用度)
# ========================================== # ==========================================
- name: "办公场景坐姿展示" - name: "办公场景服装主体坐姿展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手放在桌上或键盘上不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮的极简办公桌前,身体正对镜头微侧,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,背景为高度虚化的电脑屏幕与办公桌绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止桌面物品升高遮挡衣服下摆,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特端坐于办公桌前,上半身自然挺直微侧,双手平放键盘或桌面上远离胸口,屏幕与桌面位于画面最下沿之外,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮的极简办公桌前,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,光线均匀铺满衣服正面,背景为高度虚化的办公环境与绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。
- name: "咖啡馆坐姿场景" - name: "咖啡馆服装主体坐姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,单手端着陶瓷咖啡杯放在嘴边不遮挡印花,另一只手自然放在桌,绝对不可双手交叉抱胸。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮风格咖啡馆的窗边座位上,身体正对镜头微侧,姿态慵懒松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的柔软垂坠感。明亮自然光从窗户斜射进来,画面通透,背景为高度虚化的木质室内装潢绿植与朦胧的咖啡蒸汽。画面比例为3:4。日系胶片质感,富士Provia 400X色彩预设,35mm镜头,f/1.8大光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止咖啡杯与杯蒸汽遮挡衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特端坐于窗边座位,上半身自然挺直微侧,咖啡杯托在手心置于腰腹高度的画面下沿,杯体远离胸口区域,另一只手自然放在桌,绝对不可将杯子抬至胸前。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮风格咖啡馆的窗边座位上,姿态松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的柔软垂坠感。明亮自然光从窗户侧面射入并配合室内暖光补足衣服正面亮度,画面通透,背景为高度虚化的木质装潢绿植。画面比例为3:4。日系胶片质感,富士Provia 400X色彩预设,50mm镜头,f/1.8大光圈,8k分辨率。
- name: "书店站姿翻阅场景" - name: "书店服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捧书放在腰间不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在明亮的高大木质书架前翻阅一本精装书,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止书本遮挡衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔微侧,持书的手将书收在腰腹高度的画面下沿视线瞥向书页,另一只手自然垂放,绝对不可将书举起靠近胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮的高大木质书架前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,射灯角度经过调整避免在衣服上留下脸部阴影,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。
- name: "复古家具店站姿场景" - name: "复古家具店服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在摆满上世纪老物件的中古家具店内,眼神安静,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的质感。店内采用明亮的自然光漫反射,背景为高度虚化的胡桃木柜与明亮光斑。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止家具道具伸入前景遮挡衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,与周围家具保持安全距离,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在摆满上世纪老物件的中古家具店内,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的质感。店内采用明亮的自然光漫反射,环境家具均为背景虚化元素,不在衣服上投下明显阴影,背景为高度虚化的胡桃木柜与明亮光斑。画面比例为3:4。富士Provia 400X色彩预设,50mm镜头,f/1.8光圈,8k分辨率。
- name: "艺术画廊站姿场景" - name: "艺术画廊服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在灯光柔和明亮的现代艺术画廊内,侧身凝视一幅巨大的抽象画后转过头来正对镜头,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业顶光灯打出柔和的漫反射,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,35mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特由侧身观展状态转回正对镜头,上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在灯光柔和明亮的现代艺术画廊内,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业顶光灯打出均匀漫反射铺满衣服正面,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,50mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。
- name: "超市货架站姿场景" - name: "超市货架服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手推车不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特推着购物车或站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,姿态俏皮个性,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源打亮环境,背景为高度虚化的彩色商品与灯轨。画面比例为3:4。赛博潮流摄影,35mm镜头,f/1.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止彩色货架光线污染衣服色调,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,扶购物车的手位于画面最下沿,另一只手自然垂放,姿态俏皮个性,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源仅照亮背景,模特所在的前景由柔和补光灯独立打亮,确保衣服色彩不受环境色影响而偏色,背景为高度虚化的彩色商品。画面比例为3:4。赛博潮流摄影,50mm镜头,f/1.8光圈,8k分辨率。
# ========================================== # ==========================================
# 六、 情绪氛围与高亮特色场景区(提升品牌调性) # 六、 情绪氛围与高亮特色场景区(提升品牌调性)
# ========================================== # ==========================================
- name: "明亮工业风咖啡馆站姿展示" - name: "明亮工业风咖啡馆服装主体展示"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在明亮工业风咖啡馆的纯白水泥墙前,眼神清冷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,背景为高度虚化的明亮通道与绿植。画面比例为3:4。赛博潮流电商摄影,55mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮工业风咖啡馆的纯白水泥墙前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,衣服正面受光均匀立体,背景为高度虚化的明亮通道与绿植。画面比例为3:4。潮流电商摄影,85mm镜头,f/2.0光圈,8k分辨率。
- name: "录音棚站姿场景" - name: "录音棚服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手调音不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特戴着监听耳机站在专业麦克风前调音,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用录音棚明亮的暖色聚光灯与整体白光对比,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止耳机线垂落遮挡印花,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特佩戴监听耳机站立于麦克风架旁,麦杆收至脸侧避开胸口区域,一只手轻轻扶住耳机耳罩位于画面侧缘,另一只手自然垂放,绝对不可让手部或设备遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特在明亮的录音棚内,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用明亮的暖色聚光灯配合正面柔光板补光,确保印花区域通透明亮,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
- name: "暖光清吧坐姿场景" - name: "暖光清吧服装主体坐姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手把玩酒杯放在吧台上不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在氛围感清吧的吧台前,眼神微醺迷离,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄色点光源与背景微弱的白光对比,背景为高度虚化的酒瓶阵列与杯光交错。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止酒杯遮挡衣服,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特端坐于吧台前,上半身自然挺直微侧,酒杯托在手心置于吧台面的画面下沿高度,远离胸口,另一只手自然垂放,绝对不可举杯遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在氛围感清吧的吧台前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄主光源配合正面白色补光板,衣服正面曝光充足色彩不偏橙,背景为高度虚化的酒瓶阵列与明亮灯带。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。
- name: "日落黄昏街头站姿场景" - name: "日落黄昏街头服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手插兜不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特倚靠在城市明亮的天桥栏杆旁,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏下的视觉效果与[材质]的质感与垂感。采用日落前明亮的黄金时刻光线与边缘光,背景为高度虚化的明亮车流与城市剪影。画面比例为3:4。赛博朋克潮流摄影,35mm镜头,f/1.4大光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止剪影化处理导致衣服发黑,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身微倚天桥栏杆,肩部放松,双手自然垂放于身体两侧胯部附近,栏杆金属管避开衣服下摆高度,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特倚靠在城市明亮的天桥栏杆旁,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏暖光下的视觉效果。采用日落黄金时刻的明亮顺侧光,金色阳光直接铺满衣服正面而非逆光,色彩温暖且印花清晰锐利,背景为高度虚化的明亮车流与城市天际线。画面比例为3:4。潮流摄影,50mm镜头,f/1.4大光圈,8k分辨率。
- name: "旱冰场站姿场景" - name: "旱冰场服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特穿着旱冰鞋站在复古旱冰场中央,姿态从容,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。明亮的紫粉色调与射灯打在模特身上,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,35mm镜头,f/1.4大光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止场地彩光污染衣服原有色彩,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,姿态从容,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在复古旱冰场中央,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。场地采用明亮的主射灯独立照亮模特并配有正面柔光,衣服保持自身固有色不被环境粉紫光染色,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,50mm镜头,f/1.4大光圈,8k分辨率。
- name: "明亮现代地下车库站姿场景" - name: "明亮现代地下车库服装主体站姿场景"
prompt: | prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,眼神冷酷,身体转回正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯照明,画面清透无暗角,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。赛博朋克潮流摄影,55mm镜头,f/2.0光圈,8k分辨率。 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣,绝对禁止在外面叠加任何外套、夹克或衬衫;同时模特下半身必须穿着与衣服风格协调的深色或基础色长裤或牛仔裤,下装即使位于画面下沿之外也必须真实穿着,形成完整的日常着装状态。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止不穿下装,禁止裸露下半身,禁止画面下沿出现裸腿或内裤,禁止改变抽绳、帽绳、下摆罗纹、袖口罗纹等服装结构部件的原始颜色,禁止将印花图案或印花颜色延伸绘制到抽绳、帽子、罗纹等结构部件上,所有结构部件必须严格保持参考图中的原始同色状态。【动作硬约束】模特上半身挺拔背靠水泥柱,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯正面补光组合,画面清透无暗角,衣服受光均匀对比适中,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。潮流摄影,85mm镜头,f/2.0光圈,8k分辨率。
+13 -1
View File
@@ -39,7 +39,19 @@ CREATE TABLE SPU (
fabric_weight_unit_1 TEXT, fabric_weight_unit_1 TEXT,
lining_texture TEXT, lining_texture TEXT,
country TEXT, country TEXT,
mark TEXT mark TEXT,
placket_type TEXT DEFAULT '套头衫',
sleeve_type TEXT DEFAULT '常规袖/直筒袖',
sleeve_length_type TEXT DEFAULT '短袖',
breast_pad TEXT DEFAULT '',
silhouette TEXT DEFAULT 'H',
length_type TEXT DEFAULT '常规款',
belt TEXT DEFAULT '',
occasion TEXT DEFAULT '休闲外出',
scene TEXT DEFAULT '休闲',
hemline_shape TEXT DEFAULT '伞状',
kids_type TEXT,
kids_age TEXT
) )
""") """)
BIN
View File
Binary file not shown.
+157 -29
View File
@@ -26,6 +26,7 @@ from graph.nodes import (
template_export_node, template_export_node,
) )
from graph.state import AgentState from graph.state import AgentState
from graph.validate import with_fallback
def build_graph(): def build_graph():
@@ -59,8 +60,16 @@ def build_graph():
def _pinterest_route(state: Dict[str, Any]) -> str: def _pinterest_route(state: Dict[str, Any]) -> str:
"""图池路由:简报达标 → done;图池还有未消费图片 → analyze(继续分析,不搜索) """图池路由:简报池实时需求补——简报池还有待处理/在途简报 → 不分析不采集
图池不足 → search(新一轮搜索);轮次耗尽 → done。""" 简报池空闲 + 图池有未消费图片 → 补分析;简报池空闲 + 图池空 + 简报不足 → 搜索采集;
简报达标 → done。不按任务数量提前结束,先消耗图池存量,不够用了才主动采集。
致命 503(图像服务不可用)→ 立即终止,不再搜索/分析,直接收尾合成模板。"""
# 致命 503:图像服务不可用,重试无效 → 提前终止,未完成产品废弃,直接收尾
pipe = state.get("pinterest_pipeline")
if pipe is not None and hasattr(pipe, "is_fatal_503_aborted") and pipe.is_fatal_503_aborted():
print("[pinterest_route] ⛔ 图像服务 503 已终止任务,停止搜索/分析,直接收尾合成模板")
return "done"
target = int(state.get("pinterest_target") or 0) target = int(state.get("pinterest_target") or 0)
if target <= 0: if target <= 0:
target = 1 target = 1
@@ -72,11 +81,73 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
if max_rounds <= 0: if max_rounds <= 0:
max_rounds = max(target * 2, 5) max_rounds = max(target * 2, 5)
# 简报已达目标 → 停止补分析,直接收尾出模板(即使用户图池还有未消费图片也不再分析,
# 避免目标简报数的小任务把整池图分析成冗余简报、浪费配额)
if len(briefs) >= target: if len(briefs) >= target:
print(f"[pinterest_route] 简报已达目标 {len(briefs)}/{target}结束") print(f"[pinterest_route] 简报已达目标 {len(briefs)}/{target}停止补分析,结束(将收尾出模板)")
return "done" return "done"
# 池还有未消费图片 → 继续分析(不搜索) # 简报池还有待处理/在途简报 → 先让后台消化,不分析新图也不采集
if pipe is not None and hasattr(pipe, "pending_count"):
pending = pipe.pending_count()
if pending > 0:
# 日志由 pinterest_wait 节点进入时统一打印(阻塞等待消化,避免此处高频刷屏)
return "wait"
# 简报池空闲 + 简报未达标 + 图池还有未消费图片 → 补分析(缺口批大小由 analyze_node 按 target 收敛)
try:
from graph.pinterest import load_image_pool, load_used_images, pool_unused_images
pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "")
used = load_used_images(str(state.get("output_dir") or ""), state.get("country") or "")
unused = pool_unused_images(pool, used)
except Exception: # noqa: BLE001
unused = []
# 连续多轮图片分析无新增简报(分析失败 / 图片均不适合印花)→ 视为无法生成,
# 直接收尾,避免"取下一张参考图"式无限空转
max_empty = int(pcfg.get("max_empty_analyze") or 0)
if max_empty <= 0:
max_empty = max(target + 2, 3)
empty = int(state.get("pinterest_empty_rounds") or 0)
if empty >= max_empty:
print(f"[pinterest_route] 连续 {empty} 轮图片分析无新增简报(目标 {len(briefs)}/{target} 条简报未达标),"
f"无法生成 → 放弃补分析,直接收尾(不再逐一取下一张参考图)")
return "done"
if unused:
print(f"[pinterest_route] 简报池空闲,图池还有 {len(unused)} 张未消费图片,补分析"
f"(简报 {len(briefs)}/{target}")
return "analyze"
# 简报池空闲 + 图池空 + 简报不达标 → 新一轮搜索采集
if rounds >= max_rounds:
print(f"[pinterest_route] 已达最大轮次 {max_rounds},图池已空,简报 {len(briefs)}/{target},按现有结果继续")
return "done"
if not terms and rounds > 0:
print(f"[pinterest_route] 无可用搜索词,图池已空,停止搜索(简报 {len(briefs)}/{target}")
return "done"
print(f"[pinterest_route] 简报池空闲,图池不足,简报未达标,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target}")
return "search"
def _pinterest_custom_route(state: Dict[str, Any]) -> str:
"""自定义模式图池路由:只消耗本地图片池,从不搜索/采集。
简报池还有在途 → wait(等待后台消化);简报达标 → done;
简报池空闲 + 图池有未消费图片 → analyze;图池空 → done(自定义模式不采集)。
致命 503(图像服务不可用)→ 立即终止,直接收尾合成模板。
"""
pipe = state.get("pinterest_pipeline")
if pipe is not None and hasattr(pipe, "is_fatal_503_aborted") and pipe.is_fatal_503_aborted():
print("[pinterest_custom_route] ⛔ 图像服务 503 已终止任务,停止分析,直接收尾合成模板")
return "done"
target = int(state.get("pinterest_target") or 1) or 1
briefs = state.get("briefs") or []
if len(briefs) >= target:
print(f"[pinterest_custom_route] 简报已达目标 {len(briefs)}/{target},结束")
return "done"
if pipe is not None and hasattr(pipe, "pending_count") and pipe.pending_count() > 0:
return "wait"
try: try:
from graph.pinterest import load_image_pool, load_used_images, pool_unused_images from graph.pinterest import load_image_pool, load_used_images, pool_unused_images
pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "") pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "")
@@ -85,51 +156,79 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
unused = [] unused = []
if unused: if unused:
print(f"[pinterest_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target}") print(f"[pinterest_custom_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target}")
return "analyze" return "analyze"
print(f"[pinterest_custom_route] 图池已空,简报 {len(briefs)}/{target},结束(自定义模式不采集)")
# 图池不足 → 搜索 return "done"
if rounds >= max_rounds:
print(f"[pinterest_route] 已达最大轮次 {max_rounds},简报 {len(briefs)}/{target},按现有结果继续")
return "done"
if not terms and rounds > 0:
print(f"[pinterest_route] 无可用搜索词,停止搜索(简报 {len(briefs)}/{target}")
return "done"
print(f"[pinterest_route] 图池不足,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target}")
return "search"
def build_pinterest_graph(): def build_pinterest_graph(custom_mode: bool = False):
"""Pinterest 参考模式图(按需搜索循环 + 简报池并发生成): """Pinterest 参考模式图(按需搜索循环 + 简报池并发生成):
pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze
→ [pinterest_route] 简报不足 → 回到 pinterest_search;达标 → pinterest_finalize → [pinterest_route] 简报池还有在途 → wait(等待后台消化)→ 回到路由;
简报池空闲 + 图池有图 → analyze;图池空 + 简报不足 → search;达标 → pinterest_finalize
(排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export (排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export
custom_mode=True:自定义模式,不搜索不采集 ——
pinterest_init → pinterest_custom_load(本地文件夹校验+入库)→ pinterest_analyze
→ [pinterest_custom_route] wait / analyze / done(图池空或简报达标即结束,从不 search)。
其余下游流程(analyze→设计→三合一→OSS→种草图→模板)与普通 Pinterest 模式完全一致。
""" """
from graph.nodes import ( from graph.nodes import (
pinterest_analyze_node, pinterest_analyze_node,
pinterest_scrape_node, pinterest_scrape_node,
pinterest_search_node, pinterest_search_node,
) )
from graph.nodes import pinterest_custom_load_node
from graph.nodes.pinterest_finalize_node import pinterest_finalize_node from graph.nodes.pinterest_finalize_node import pinterest_finalize_node
from graph.nodes.pinterest_init_node import pinterest_init_node from graph.nodes.pinterest_init_node import pinterest_init_node
@with_fallback("pinterest_wait")
def _pinterest_wait(state: Dict[str, Any]) -> Dict[str, Any]:
"""简报池还有在途简报时阻塞等待后台消化(最多 60s),避免轮询刷屏。
用 pipeline.wait_idle 的 Condition 阻塞等待(_process_one 完成即唤醒),
消化完才返回,路由重新判断;超时兜底返回,避免死等。
"""
pipe = state.get("pinterest_pipeline")
if pipe is not None and hasattr(pipe, "wait_idle"):
pipe.wait_idle(60)
else:
import time as _t
_t.sleep(5)
return {}
builder = StateGraph(AgentState) builder = StateGraph(AgentState)
builder.add_node("pinterest_init", pinterest_init_node) builder.add_node("pinterest_init", pinterest_init_node)
builder.add_node("pinterest_search", pinterest_search_node)
builder.add_node("pinterest_scrape", pinterest_scrape_node)
builder.add_node("pinterest_analyze", pinterest_analyze_node) builder.add_node("pinterest_analyze", pinterest_analyze_node)
builder.add_node("pinterest_wait", _pinterest_wait)
builder.add_node("pinterest_finalize", pinterest_finalize_node) builder.add_node("pinterest_finalize", pinterest_finalize_node)
builder.add_node("template_export", template_export_node) builder.add_node("template_export", template_export_node)
builder.add_edge("__start__", "pinterest_init") if custom_mode:
builder.add_edge("pinterest_init", "pinterest_search") builder.add_node("pinterest_custom_load", pinterest_custom_load_node)
builder.add_edge("pinterest_search", "pinterest_scrape") builder.add_edge("__start__", "pinterest_init")
builder.add_edge("pinterest_scrape", "pinterest_analyze") builder.add_edge("pinterest_init", "pinterest_custom_load")
builder.add_conditional_edges("pinterest_analyze", _pinterest_route, { builder.add_edge("pinterest_custom_load", "pinterest_analyze")
"analyze": "pinterest_analyze", # 图池还有未消费图片 → 继续分析(不搜索) builder.add_conditional_edges("pinterest_analyze", _pinterest_custom_route, {
"search": "pinterest_search", # 图池不足 → 新一轮搜索 "analyze": "pinterest_analyze",
"done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池) "wait": "pinterest_wait",
}) "done": "pinterest_finalize",
})
else:
builder.add_node("pinterest_search", pinterest_search_node)
builder.add_node("pinterest_scrape", pinterest_scrape_node)
builder.add_edge("__start__", "pinterest_init")
builder.add_edge("pinterest_init", "pinterest_search")
builder.add_edge("pinterest_search", "pinterest_scrape")
builder.add_edge("pinterest_scrape", "pinterest_analyze")
builder.add_conditional_edges("pinterest_analyze", _pinterest_route, {
"analyze": "pinterest_analyze", # 简报池空闲 + 图池有未消费图片 → 补分析(不搜索)
"search": "pinterest_search", # 简报池空闲 + 图池空 + 简报不足 → 新一轮搜索
"wait": "pinterest_wait", # 简报池还有在途 → 等待后台消化
"done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池)
})
builder.add_edge("pinterest_wait", "pinterest_analyze")
builder.add_edge("pinterest_finalize", "template_export") builder.add_edge("pinterest_finalize", "template_export")
builder.add_edge("template_export", END) builder.add_edge("template_export", END)
return builder.compile() return builder.compile()
@@ -194,14 +293,39 @@ def run_pinterest_ref(
project_root: Path, project_root: Path,
output_root: Optional[Path] = None, output_root: Optional[Path] = None,
task_timestamp: Optional[str] = None, task_timestamp: Optional[str] = None,
custom_image_dir: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Pinterest 参考模式入口:独立于 Google Trends 的完整流程。 """Pinterest 参考模式入口:独立于 Google Trends 的完整流程。
种子词 → LLM 搜索词(json_schema + 动态注入防重复)→ 爬图 → LLM 分析图片 种子词 → LLM 搜索词(json_schema + 动态注入防重复)→ 爬图 → LLM 分析图片
→ 设计简报 → 设计稿 → 产品图 → 上传 → 种草图 → 模板导出。 → 设计简报 → 设计稿 → 产品图 → 上传 → 种草图 → 模板导出。
参数语义与 run_country 一致(project_root=数据根,output_root=产物根)。 参数语义与 run_country 一致(project_root=数据根,output_root=产物根)。
custom_image_dir 非空 或 config.pinterest.mode=="custom" → 进入自定义模式:
不搜索不采集,直接把指定文件夹的有效图片送多模态分析,沿用 Pinterest 后续所有步骤。
数量硬校验(有效图片数 ≥ 选品清单总数)在启动前执行,不满足直接抛错。
""" """
compiled = build_pinterest_graph() target = len((global_config.get("product") or {}).get("spu_tasks") or []) \
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1
pinterest_cfg = global_config.get("pinterest") or {}
mode = str(pinterest_cfg.get("mode") or "scrape").strip().lower()
custom_dir = str(custom_image_dir or "").strip() \
or str(pinterest_cfg.get("custom_image_dir") or "").strip()
custom_mode = bool(custom_dir) or (mode == "custom")
if custom_mode:
if not custom_dir:
raise ValueError(
"自定义模式需要填写 pinterest.custom_image_dir(选择上传的图片文件夹),"
"当前为空,无法启动")
from graph.pinterest import validate_custom_images
ok, valid_n, msg = validate_custom_images(custom_dir, target)
if not ok:
raise ValueError(f"自定义模式数量校验未通过:{msg}(选品清单总数 {target}")
global_config.setdefault("pinterest", {})["custom_image_dir"] = custom_dir
print(f"[run_pinterest_ref] 自定义模式启用:{custom_dir} 有效图片 {valid_n} 张 ≥ 选品清单 {target}")
compiled = build_pinterest_graph(custom_mode=custom_mode)
cc = build_country_config(global_config, country, project_root) cc = build_country_config(global_config, country, project_root)
prompts_dir = project_root / "prompts" / country prompts_dir = project_root / "prompts" / country
cache_dir = (output_root or project_root) / "output" / country cache_dir = (output_root or project_root) / "output" / country
@@ -232,5 +356,9 @@ def run_pinterest_ref(
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1, or int((global_config.get("product") or {}).get("spu_count") or 0) or 1,
"pinterest_rounds": 0, "pinterest_rounds": 0,
"pinterest_attempted": [], "pinterest_attempted": [],
"pinterest_images": {},
"pinterest_briefs": [],
"pinterest_login": {},
"pinterest_pipeline": None,
} }
return compiled.invoke(state) return compiled.invoke(state)
+95 -26
View File
@@ -10,6 +10,7 @@
所有请求跳过环境代理(NO_PROXY),适配用户挂 VPN 时直连国内/自建网关。 所有请求跳过环境代理(NO_PROXY),适配用户挂 VPN 时直连国内/自建网关。
""" """
import base64 import base64
import json
import time import time
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -31,6 +32,17 @@ _FINAL_STATUS = ("succeeded", "completed", "done")
_FAIL_STATUS = ("failed", "error") _FAIL_STATUS = ("failed", "error")
def _retry_after(resp, attempt: int) -> int:
"""429 限流等待秒数:优先取网关 Retry-After 头,否则指数退避(3/6/9s)。"""
ra = resp.headers.get("Retry-After")
try:
if ra is not None and ra.isdigit():
return min(int(ra), 30)
except Exception: # noqa: BLE001
pass
return 3 * (attempt + 1)
def _shrink_blob_to_2mb(img_path: str, blob: bytes, max_bytes: int = 2 * 1024 * 1024) -> bytes: def _shrink_blob_to_2mb(img_path: str, blob: bytes, max_bytes: int = 2 * 1024 * 1024) -> bytes:
"""把大图压缩到 <max_bytes(默认 2MB)再上传: """把大图压缩到 <max_bytes(默认 2MB)再上传:
- 尺寸过大先缩放(合成输入 1600x2200 内足够); - 尺寸过大先缩放(合成输入 1600x2200 内足够);
@@ -66,29 +78,32 @@ def _shrink_blob_to_2mb(img_path: str, blob: bytes, max_bytes: int = 2 * 1024 *
return blob return blob
def _save_from_response(j: dict, out_path: str) -> str: def _save_from_response(j: dict, out_path: str, started: float = None) -> str:
"""从同步/异步最终响应提取图片(data[].b64_json 或 url)并保存。""" """从同步/异步最终响应提取图片(data[].b64_json 或 url)并保存。"""
data_item = (j.get("data") or [{}])[0] data_item = (j.get("data") or [{}])[0]
b64 = data_item.get("b64_json") b64 = data_item.get("b64_json")
if b64: if b64:
Path(out_path).parent.mkdir(parents=True, exist_ok=True) Path(out_path).parent.mkdir(parents=True, exist_ok=True)
Path(out_path).write_bytes(base64.b64decode(b64)) Path(out_path).write_bytes(base64.b64decode(b64))
return out_path else:
url = data_item.get("url") url = data_item.get("url")
if not url: if not url:
raise RuntimeError(f"图像 API 返回无 b64_json/url: {str(j)[:200]}") raise RuntimeError(f"图像 API 返回无 b64_json/url: {str(j)[:200]}")
img_resp = requests.get(url, proxies=NO_PROXY, timeout=120) img_resp = requests.get(url, proxies=NO_PROXY, timeout=120)
img_resp.raise_for_status() img_resp.raise_for_status()
Path(out_path).parent.mkdir(parents=True, exist_ok=True) Path(out_path).parent.mkdir(parents=True, exist_ok=True)
Path(out_path).write_bytes(img_resp.content) Path(out_path).write_bytes(img_resp.content)
if started is not None:
print(f"[img] 已生成(耗时 {time.time() - started:.0f}s: {out_path}")
return out_path return out_path
def _wait_task(base_url: str, headers: dict, task_id: str, poll_after_ms: int, def _wait_task(base_url: str, headers: dict, task_id: str, poll_after_ms: int,
timeout: int = 60) -> dict: timeout: int = 300) -> dict:
"""轮询异步图像任务直到完成;uncertain(结果暂时不确定)继续等,不重复提交。 """轮询异步图像任务直到完成;uncertain(结果暂时不确定)继续等,不重复提交。
timeout 默认 60s:异步路径不稳定(ai-media 网关常 uncertain/task not found), timeout 默认 300s(与提交请求 timeout 一致):网关异步任务实际需 30s~6 分钟
超时即抛异常由调用方重试同步提交""" (设计稿/三图合成),60s 轮询超时会把仍在处理中的任务误判为失败而重复提交
白白消耗配额和时间。超时即抛异常由调用方重试同步提交。"""
deadline = time.time() + timeout deadline = time.time() + timeout
last = "" last = ""
while time.time() < deadline: while time.time() < deadline:
@@ -114,14 +129,15 @@ def _wait_task(base_url: str, headers: dict, task_id: str, poll_after_ms: int,
raise RuntimeError(f"图像异步任务超时({timeout}s") raise RuntimeError(f"图像异步任务超时({timeout}s")
def _resolve_task_or_sync(j: dict, base_url: str, headers: dict, out_path: str) -> str: def _resolve_task_or_sync(j: dict, base_url: str, headers: dict, out_path: str,
started: float = None) -> str:
"""提交后的统一处理:异步任务则轮询,然后提取图片保存。""" """提交后的统一处理:异步任务则轮询,然后提取图片保存。"""
if j.get("object") == "image.task" or j.get("task_id") or j.get("id", "").startswith("imgtask"): if j.get("object") == "image.task" or j.get("task_id") or j.get("id", "").startswith("imgtask"):
tid = j.get("task_id") or j.get("id") or "" tid = j.get("task_id") or j.get("id") or ""
if not tid: if not tid:
raise RuntimeError(f"异步任务无 task_id: {str(j)[:200]}") raise RuntimeError(f"异步任务无 task_id: {str(j)[:200]}")
j = _wait_task(base_url, headers, tid, int(j.get("poll_after_ms") or 2000)) j = _wait_task(base_url, headers, tid, int(j.get("poll_after_ms") or 2000))
return _save_from_response(j, out_path) return _save_from_response(j, out_path, started=started)
class OpenAIImageBackend(ImageBackend): class OpenAIImageBackend(ImageBackend):
@@ -171,16 +187,36 @@ class OpenAIImageBackend(ImageBackend):
"n": 1, "n": 1,
"size": size or cfg.get("size", "1024x1024"), "size": size or cfg.get("size", "1024x1024"),
"model": model, "model": model,
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
} }
# execution_mode 仅对明确支持的网关(如 ai-media)传;yunfei 等标准 OpenAI 网关
# 不认识该参数,硬传会导致空响应,故默认不传,仅 config 显式配置时才带上
em = str(cfg.get("execution_mode") or "").strip()
if em:
data["execution_mode"] = em
if seed is not None: if seed is not None:
data["seed"] = seed data["seed"] = seed
started = time.time()
print(f"[img] 开始图生图({model}{data['size']},参考图 {len(file_payloads)} 张)→ {Path(out_path).name}")
# 提交重试:异步路径不稳定 → 失败重试同步提交(最多 3 次); # 提交重试:异步路径不稳定 → 失败重试同步提交(最多 3 次);
# 内容政策拦截(content_policy_violation)多为网关误判 → 等待后重试 # 内容政策拦截(content_policy_violation)多为网关误判 → 等待后重试
last_err: Optional[str] = None last_err: Optional[str] = None
for attempt in range(3): for attempt in range(3):
resp = requests.post(f"{base_url}/images/edits", headers=headers, files=files, resp = requests.post(f"{base_url}/images/edits", headers=headers, files=files,
data=data, timeout=300, proxies=NO_PROXY) data=data, timeout=300, proxies=NO_PROXY)
if resp.status_code == 429:
# 限流:尊重网关负载退避等待再重试,避免硬撞雪崩
wait = _retry_after(resp, attempt)
print(f"[img] 图像 API 429 限流,等待 {wait}s 重试 {attempt + 1}/3")
time.sleep(wait)
continue
if resp.status_code >= 500:
# 5xx 服务端错误(500/502/503/504 超时等):网关临时故障/过载,退避后重试
body = resp.text or ""
last_err = f"图像 API {resp.status_code}: {body[:300]}"
wait = 3 * (attempt + 1)
print(f"[img] 图像 API {resp.status_code}(网关临时故障/超时),等待 {wait}s 重试 {attempt + 1}/3")
time.sleep(wait)
continue
if resp.status_code >= 400: if resp.status_code >= 400:
body = resp.text or "" body = resp.text or ""
if "content_policy" in body and attempt < 2: if "content_policy" in body and attempt < 2:
@@ -192,19 +228,26 @@ class OpenAIImageBackend(ImageBackend):
continue continue
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}") raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
try: try:
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path) return _resolve_task_or_sync(resp.json(), base_url, headers, out_path,
started=started)
except json.JSONDecodeError as e:
# 空/非 JSON 响应:多为网关过载返回空 body → 退避后重试,
# 避免即时重压触发网关 429(用户实测空响应风暴 → 429)
last_err = str(e)
wait = 3 * (attempt + 1)
print(f"[img] 网关空响应(JSON 解析失败),等待 {wait}s 重试提交 {attempt + 1}/3: {e}")
time.sleep(wait)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
last_err = str(e) last_err = str(e)
print(f"[img] 第 {attempt + 1} 次提交异步失败,重试同步提交: {e}") print(f"[img] 第 {attempt + 1} 次提交异步失败,等待后重试同步提交: {e}")
time.sleep(2 * (attempt + 1))
raise RuntimeError(f"图像合成多次提交均失败: {last_err}") raise RuntimeError(f"图像合成多次提交均失败: {last_err}")
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "", def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "",
seed: Optional[int] = None) -> str: seed: Optional[int] = None) -> str:
"""纯文生图:生成白底纯印花设计稿(standalone pure print design)。 """纯文生图:生成白底纯印花设计稿(standalone pure print design)。
size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。 size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。 seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。"""
background: 配置 compose.background="transparent" 时传 background 参数 → 透明背景 PNG
gpt-image-1/2 等模型支持;网关不支持该参数时会被忽略或由网关兜底)。"""
cfg = self._cfg cfg = self._cfg
api_key = cfg.get("api_key", "") api_key = cfg.get("api_key", "")
model = cfg.get("model", "gpt-image-1") model = cfg.get("model", "gpt-image-1")
@@ -220,17 +263,34 @@ class OpenAIImageBackend(ImageBackend):
"size": size or cfg.get("size", "1024x1024"), "size": size or cfg.get("size", "1024x1024"),
"model": model, "model": model,
"response_format": "b64_json", "response_format": "b64_json",
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
} }
# execution_mode 仅对明确支持的网关(如 ai-media)传;yunfei 等标准 OpenAI 网关
# 不认识该参数,硬传会导致空响应,故默认不传,仅 config 显式配置时才带上
em = str(cfg.get("execution_mode") or "").strip()
if em:
data["execution_mode"] = em
if seed is not None: if seed is not None:
data["seed"] = seed data["seed"] = seed
bg = str(cfg.get("background") or "").strip() started = time.time()
if bg: print(f"[img] 开始文生图({model}{data['size']})→ {Path(out_path).name}")
data["background"] = bg # 如 "transparent"(透明背景 PNG
last_err: Optional[str] = None last_err: Optional[str] = None
for attempt in range(3): for attempt in range(3):
resp = requests.post(f"{base_url}/images/generations", headers=headers, json=data, resp = requests.post(f"{base_url}/images/generations", headers=headers, json=data,
timeout=300, proxies=NO_PROXY) timeout=300, proxies=NO_PROXY)
if resp.status_code == 429:
# 限流:尊重网关负载退避等待再重试,避免硬撞雪崩
wait = _retry_after(resp, attempt)
print(f"[img] 图像 API 429 限流,等待 {wait}s 重试 {attempt + 1}/3")
time.sleep(wait)
continue
if resp.status_code >= 500:
# 5xx 服务端错误(500/502/503/504 超时等):网关临时故障/过载,退避后重试
body = resp.text or ""
last_err = f"图像 API {resp.status_code}: {body[:300]}"
wait = 3 * (attempt + 1)
print(f"[img] 图像 API {resp.status_code}(网关临时故障/超时),等待 {wait}s 重试 {attempt + 1}/3")
time.sleep(wait)
continue
if resp.status_code >= 400: if resp.status_code >= 400:
body = resp.text or "" body = resp.text or ""
if "content_policy" in body and attempt < 2: if "content_policy" in body and attempt < 2:
@@ -242,8 +302,17 @@ class OpenAIImageBackend(ImageBackend):
continue continue
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}") raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
try: try:
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path) return _resolve_task_or_sync(resp.json(), base_url, headers, out_path,
started=started)
except json.JSONDecodeError as e:
# 空/非 JSON 响应:多为网关过载返回空 body → 退避后重试,
# 避免即时重压触发网关 429(用户实测空响应风暴 → 429)
last_err = str(e)
wait = 3 * (attempt + 1)
print(f"[img] 网关空响应(JSON 解析失败),等待 {wait}s 重试生成 {attempt + 1}/3: {e}")
time.sleep(wait)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
last_err = str(e) last_err = str(e)
print(f"[img] 第 {attempt + 1} 次生成异步失败,重试同步提交: {e}") print(f"[img] 第 {attempt + 1} 次生成异步失败,等待后重试同步提交: {e}")
time.sleep(2 * (attempt + 1))
raise RuntimeError(f"图像生成多次提交均失败: {last_err}") raise RuntimeError(f"图像生成多次提交均失败: {last_err}")
+14 -14
View File
@@ -152,42 +152,42 @@ class MockBackend:
seeds = [str(s).strip() for s in (context.get("seeds") or []) if str(s).strip()] seeds = [str(s).strip() for s in (context.get("seeds") or []) if str(s).strip()]
used = {str(u).strip().lower() for u in (context.get("used_terms") or [])} used = {str(u).strip().lower() for u in (context.get("used_terms") or [])}
count = int(context.get("count", 10)) count = int(context.get("count", 10))
suffix = str(context.get("search_term_suffix") or "").strip()
pool = [s for s in seeds if s.lower() not in used] pool = [s for s in seeds if s.lower() not in used]
random.shuffle(pool) random.shuffle(pool)
terms = pool[:count] terms = pool[:count]
# 不足时用「种子词 + 风格词」组合补足(视觉导向,避免与已用重复) # 不足时用「种子词 + 风格词」组合补足(视觉导向,避免与已用重复)
style_tail = ["t-shirt design", "graphic tee", "print art", "vintage tee", "flat design"] style_tail = ["graphic tee", "print art", "vintage tee", "flat design"]
i = 0 i = 0
while len(terms) < count and pool: while len(terms) < count and pool:
combo = f"{pool[i % len(pool)]} {style_tail[(i // len(pool)) % len(style_tail)]}" combo = f"{pool[i % len(pool)]} {style_tail[(i // len(pool)) % len(style_tail)]}"
if combo.lower() not in used and combo not in terms: if combo.lower() not in used and combo not in terms:
terms.append(combo) terms.append(combo)
i += 1 i += 1
# 自动追加 " t-shirt design":让 Pinterest 返回真正的 T 恤印花图(更适合作印花设计参考) # 自动追加自定义后缀(config.pinterest.search_term_suffix,默认 t-shirt design):
terms = [f"{t} t-shirt design" if "t-shirt design" not in t.lower() else t for t in terms] # 让 Pinterest 返回真正的 T 恤印花图(更适合作印花设计参考)
if suffix:
terms = [f"{t} {suffix}" if suffix not in t.lower() else t for t in terms]
return {"search_terms": terms} return {"search_terms": terms}
def analyze_pinterest_images(self, image_paths, term="", country="", on_400=None): def analyze_pinterest_images(self, image_paths, term="", country="", on_400=None, custom_mode=False):
"""规则生成设计简报(零 API 成本):按搜索词启发式推导风格/配色/构图。""" """规则生成设计简报(零 API 成本):按搜索词启发式推导风格/配色/构图。
custom_mode: 仅作签名兼容(mock 为规则生成,不区分模式)。
"""
from ..classify import classify, prompt_suggestion from ..classify import classify, prompt_suggestion
cat = classify(term) cat = classify(term)
art_style, palette = derive_style_palette(term, country, category=cat) art_style, palette = derive_style_palette(term, country, category=cat)
motif = prompt_suggestion(term, cat).split(" --no ")[0].split(",")[0].strip() motif = prompt_suggestion(term, cat).split(" --no ")[0].split(",")[0].strip()
composition = derive_composition(term, cat) composition = derive_composition(term, cat)
negative = ("no real people, no likeness of any person, no copyrighted characters, "
"no brand logos, no trademarks, no celebrity, no readable text unless safe")
n = max(1, len(image_paths or [])) n = max(1, len(image_paths or []))
paths = list(image_paths or []) paths = list(image_paths or [])
return [{ return [{
"topic": term, "topic": term,
"concept": f"(启发式兜底)围绕「{term}」做原创{art_style}风格印花", "suitable_for_print": True,
"motif": motif,
"art_style": art_style,
"color_palette": palette,
"composition": composition,
"negative_prompt": negative,
"image_prompt": (f"{motif}, {art_style}, {palette}, {composition}, " "image_prompt": (f"{motif}, {art_style}, {palette}, {composition}, "
f"original {art_style} t-shirt print design"), f"original {art_style} t-shirt print design, "
f"no brand logo, no trademark, no character, no watermark"),
# 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应) # 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应)
"ref_images": [str(paths[i])] if i < len(paths) else [], "ref_images": [str(paths[i])] if i < len(paths) else [],
"source": "pinterest", "source": "pinterest",
+227 -87
View File
@@ -13,11 +13,12 @@ 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 等外部采集)。
from .base import LLMBackend from .base import LLMBackend
from graph.paths import runtime_root from graph.paths import project_root, runtime_root
# 直连策略:忽略环境代理(用户挂 VPN 时代理会拦截国内/自建网关的请求) # 直连策略:忽略环境代理(用户挂 VPN 时代理会拦截国内/自建网关的请求)
NO_PROXY = {"http": None, "https": None} NO_PROXY = {"http": None, "https": None}
@@ -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 路由到模板 2ES 路由到模板 3,其余默认模板 1;后续可按国家新增模板 # 内置默认路由(config.yaml title_templates.route 缺失/留空时回退;JP→模板2ES模板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:
@@ -260,7 +292,7 @@ that are DIRECTLY usable as reference for a t-shirt print design.
RULES: RULES:
- Generate EXACTLY the requested number of search terms (usually 1 per call). - Generate EXACTLY the requested number of search terms (usually 1 per call).
- Every term MUST be a "t-shirt design" style query: think of it as if the user typed "<concept> t-shirt design" on - Every term MUST be a "__SUFFIX__" style query: think of it as if the user typed "<concept>__SUFFIX__" on
Pinterest, so the scraped images are actual t-shirt graphics / flat print artworks, NOT lifestyle photos, scenery, Pinterest, so the scraped images are actual t-shirt graphics / flat print artworks, NOT lifestyle photos, scenery,
architecture, food plates, or anything that cannot become a clean chest print. architecture, food plates, or anything that cannot become a clean chest print.
- Terms MUST be suitable for a SHORT-SLEEVE T-SHIRT PRINT: a flat, graphic, print-ready concept (illustration, mascot, - Terms MUST be suitable for a SHORT-SLEEVE T-SHIRT PRINT: a flat, graphic, print-ready concept (illustration, mascot,
@@ -309,7 +341,7 @@ def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
f"Generate {count} new, diverse, non-overlapping Pinterest search term(s) " f"Generate {count} new, diverse, non-overlapping Pinterest search term(s) "
f"that are suitable for a SHORT-SLEEVE T-SHIRT PRINT design " f"that are suitable for a SHORT-SLEEVE T-SHIRT PRINT design "
f"(flat, graphic, print-ready motif that works as a chest print). " f"(flat, graphic, print-ready motif that works as a chest print). "
f"Each term should read like \"<concept> t-shirt design\" so Pinterest returns " f"Each term should read like \"<concept>__SUFFIX__\" so Pinterest returns "
f"actual t-shirt graphics / flat print artwork as reference.", f"actual t-shirt graphics / flat print artwork as reference.",
] ]
return "\n".join(lines) return "\n".join(lines)
@@ -318,31 +350,52 @@ def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
# —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)—— # —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)——
# image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接), # image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接),
# 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。 # 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD (print-on-demand) T-shirt design analyst. PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD T-shirt design analyst. Given ONE Pinterest reference image,
You receive Pinterest reference images for one search term. For each image, extract the VISUAL CONCEPT judge whether it can inspire a T-shirt print, then write an ORIGINAL design brief
(style, mood, motif, color palette, composition) that makes it appealing, then produce an ORIGINAL capturing its vibe WITHOUT copying.
T-shirt print design brief that captures that VIBE WITHOUT copying the image. Your image_prompt will be sent TOGETHER WITH this reference image to an image
generator, so it must actively override visual imitation.
RULES: RULES:
- NEVER copy the image, never reproduce the exact artwork, characters, logos, or any text from it. 1. NO COPYING — never reproduce or closely imitate the reference's artwork,
- Extract only the abstract style/mood/motif concept as inspiration. characters, layout or text. Deliberately change motif, arrangement and/or
- Produce an original, flat, print-ready design brief (no garment, no model, no background scene). palette so the two read as clearly different works sharing only a general
- COPYRIGHT-SAFE: no brands, no logos, no characters, no celebrities, no real persons, no franchises. style. Distill inspiration into generic style words (retro, y2k, minimal,
- AVOID: politics, religion, hate, violence, sexual content, alcohol, national flags. grunge, boho, kawaii...); never imitate an identifiable artist/studio/IP
- motif: English, concrete central subject of the print (e.g. "a smiling cat with a fish", "geometric mountain layers"). style.
- art_style: English visual technique (e.g. "clean flat vector", "retro screen print"). 2. FORBIDDEN — brand logos, trademarks, slogans, mascots, copyrighted
- color_palette: English colors (e.g. "sunset orange, cream, dusty blue"). characters, real people/celebrities, movie/game/anime/band IP, lyrics,
- composition: English layout (e.g. "centered emblem with balanced negative space"). even stylized or silhouette versions. Avoid politics, religion, violence,
- concept: Chinese, one sentence describing the design idea. sexual content, alcohol, drugs, gambling, flags, death/occult themes.
- negative_prompt: what to avoid (real people, likeness, characters, logos, text). 3. FORM — ONE clear central subject with strong graphic composition;
- image_prompt: a COMPLETE, fluent English text-to-image prompt for generating the ORIGINAL flat print print-ready standalone artwork. ANY colors are fine — rich palettes,
design artwork (the print itself, NOT a garment photo). Describe the motif, art style, colors, layout gradients and detailed shading are all acceptable. Photographic references
and mood in natural English, as a standalone print. Do NOT include garment / shirt / model / mannequin / may be rendered as detailed full-color illustrations, retro badges or
background-scene / watermark words. Do NOT mention any size or white-background suffix — a fixed vintage stickers.
"small centered print on pure white" suffix will be appended automatically by the system.
Return JSON with the field "designs" (array of objects with keys: TEXT: short ORIGINAL English wording (1-6 words) allowed; wrap exact words in
motif, art_style, color_palette, composition, concept, negative_prompt, image_prompt).""" double quotes and demand exact spelling; integrate into composition. Never
reuse/translate reference text; no brand/band/movie names or famous slogans.
When unsure, omit.
suitable_for_print: DEFAULT TRUE for graphics, illustrations, badges, vector
art, typography posters, or prints on mockups (judge only the printed artwork).
FALSE only for: subjectless photo scenery, memes/screenshots/collages,
watermarked or very low-quality images, decor/food/candid photos with no
usable motif. Even when FALSE, still fill all fields so downstream never breaks.
image_prompt = two parts:
1) mandatory opener, e.g.: "Use the attached reference image only as loose
inspiration for overall mood, theme and era — do NOT reproduce, trace,
rearrange, recolor or closely imitate any element, character, layout or
text shown in it."
2) the new design: [central motif] + [style] + [color treatment] +
[composition] + [mood], plus quoted original text if used.
NEVER mention shirts, apparel, models, scenes, sizes, backgrounds or
watermarks — placement is handled externally.
OUTPUT — ONLY valid JSON, no fences:
{"designs":[{"suitable_for_print":<bool>,"image_prompt":"<str>"}]}"""
PINTEREST_ANALYZE_SCHEMA = { PINTEREST_ANALYZE_SCHEMA = {
"name": "pinterest_design_briefs", "name": "pinterest_design_briefs",
@@ -354,17 +407,10 @@ PINTEREST_ANALYZE_SCHEMA = {
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"image_index": {"type": "integer"}, "suitable_for_print": {"type": "boolean"},
"motif": {"type": "string"},
"art_style": {"type": "string"},
"color_palette": {"type": "string"},
"composition": {"type": "string"},
"concept": {"type": "string"},
"negative_prompt": {"type": "string"},
"image_prompt": {"type": "string"}, "image_prompt": {"type": "string"},
}, },
"required": ["image_index", "motif", "art_style", "color_palette", "required": ["suitable_for_print", "image_prompt"],
"composition", "concept", "negative_prompt", "image_prompt"],
"additionalProperties": False, "additionalProperties": False,
}, },
} }
@@ -375,18 +421,93 @@ PINTEREST_ANALYZE_SCHEMA = {
} }
def build_pinterest_analyze_user_prompt(term: str, country: str, image_count: int) -> str: def _pinterest_analyze_prompt_file(country: str, filename: str) -> str:
"""定位多模态分析提示词文件:优先运行根 prompts(exe 旁,可编辑),回退数据根 prompts。
支持国家覆盖(prompts/<country>/<filename>)优先于全局(prompts/<filename>)。
找不到则返回空串,由调用方回退内置默认。
"""
for base in (runtime_root(), project_root()):
if country:
p = base / "prompts" / country / filename
if p.exists():
return p.read_text(encoding="utf-8").strip()
p = base / "prompts" / filename
if p.exists():
return p.read_text(encoding="utf-8").strip()
return ""
# 自定义模式(pinterest.mode=custom)多模态分析系统提示词内置默认:
# 输入是「现有爆款产品图」(模特实拍/平铺图),定位衣服上的印花并产出同风格、不同执行的非侵权原创设计。
# 该文本为「内置默认」,可被 prompts/custom_analyze_system.md 覆盖(缺失/留空回退这里)。
CUSTOM_ANALYZE_SYSTEM_PROMPT = """You are a POD T-shirt design analyst working in CUSTOM-REFERENCE mode. You are given ONE existing bestseller product photo — an on-model shot or a flat-lay shot of a printed garment. Your job:
1. Locate the printed artwork on the garment and judge whether that PRINT can inspire a new T-shirt design (ignore the shirt itself, the model, the background, watermarks and photo quality — judge only the printed artwork).
2. Write an ORIGINAL design brief that keeps the bestseller's general vibe (theme, style genre, era, mood) but is a clearly different, non-infringing work.
RULES:
0. REFERENCE HANDLING — the attached photo is a product mockup of an existing bestseller, not an artwork file. Use it ONLY to identify the printed artwork and distill its appeal into generic style keywords (retro, y2k, minimal, grunge, boho, kawaii, western, cottagecore, vintage cartoon...). Never reproduce, trace, rearrange, recolor or closely imitate the print, its layout, characters or text.
1. SAME SUBJECT FAMILY, NEW EXECUTION — the new design must belong to the same visual family as the bestseller print so a buyer would instantly see they are "same style, different shirt". LOCK these elements to the bestseller's: (a) general subject category (e.g., human/animal figure silhouette, celestial object, floral, vehicle, typography-led...), (b) art technique (e.g., grunge ink silhouette, clean line art, watercolor, distressed collage...), (c) palette (monochrome / muted / neon...). VARY these: the exact subject, the pose, the composition and layout, and any decorative framing. The result must be clearly original — no traced or rearranged reuse of the reference artwork — while staying recognizably in the same genre.
2. IP SCREENING — if the bestseller print contains brand logos, trademarks, slogans, mascots, copyrighted characters (including stylized or silhouette versions), real people/celebrities, movie/game/anime/band IP, or lyrics, do NOT imitate them; swap in fully generic equivalents (e.g., an unnamed cartoon animal instead of a recognizable mascot). Also avoid politics, religion, violence, sexual content, alcohol, drugs, gambling, flags, death/occult themes.
3. FORM — ONE clear central subject with strong graphic composition; print-ready standalone artwork. ANY colors are fine — rich palettes, gradients and detailed shading are all acceptable. Because the reference is a product photo, explicitly exclude the garment and the photo itself: never describe shirts, models, hangers, scenes or backgrounds. Photographic elements in the print may be re-rendered as detailed full-color illustrations, retro badges or vintage stickers.
4. TEXT — short ORIGINAL English wording (1-6 words) allowed; wrap exact words in double quotes and demand exact spelling; integrate into composition. Never reuse, translate or near-duplicate the bestseller print's wording; no brand/band/movie names or famous slogans. When unsure, omit.
suitable_for_print: judge ONLY the printed artwork extracted from the bestseller photo. DEFAULT TRUE for graphics, illustrations, badges, vector art, typography prints, or prints visible on mockups. FALSE only for: blank garments with no artwork, prints that are indiscernible due to watermark, blur or very low quality, pure subjectless photo scenery, memes/screenshots/collages with no usable motif. Even when FALSE, still fill all fields so downstream never breaks.
image_prompt = two parts:
1) mandatory opener: "First, carefully examine the printed artwork on the garment in the attached photo: zoom in mentally on the print area, identify its subject, technique, palette and layout, and base the new design on THOSE observed traits. Ignore the model, background and photo quality. Then: use the attached bestseller product photo only as loose inspiration for overall mood, theme, era and style genre — do NOT reproduce, trace, rearrange, recolor or closely imitate its printed artwork, characters, layout or text, and do NOT render a shirt, garment, model, hanger, photo scene, product mockup or background of any kind."
2) the new design: [same subject family as observed] + [same technique and palette family] + [changed exact subject, pose, composition and framing] + [mood], plus quoted original text if used. NEVER mention shirts, apparel, models, scenes, sizes, backgrounds or watermarks — placement is handled externally. Before output, verify the new design shares the locked elements (subject category, technique, palette) with the observed print; if not, revise it.
OUTPUT — ONLY valid JSON, no fences:
{"designs":[{"suitable_for_print":<bool>,"image_prompt":"<str>"}]}"""
def _custom_analyze_prompt_file(filename: str) -> str:
"""定位自定义模式多模态分析提示词文件:优先运行根 prompts(exe 旁,可编辑),回退数据根 prompts。
只读全局 prompts/<filename>(自定义模式不按国家区分)。找不到返回空串,由调用方回退内置默认。
"""
for base in (runtime_root(), project_root()):
p = base / "prompts" / filename
if p.exists():
return p.read_text(encoding="utf-8").strip()
return ""
def resolve_custom_analyze_system_prompt() -> str:
"""自定义模式多模态分析系统提示词(可配置):命中 prompts/custom_analyze_system.md
否则回退内置 CUSTOM_ANALYZE_SYSTEM_PROMPT。"""
text = _custom_analyze_prompt_file("custom_analyze_system.md")
return text if text else CUSTOM_ANALYZE_SYSTEM_PROMPT
def build_custom_analyze_user_prompt() -> str:
"""自定义模式多模态分析用户提示词(可配置):命中 prompts/custom_analyze_user.md 否则内置默认。"""
text = _custom_analyze_prompt_file("custom_analyze_user.md")
if text:
return text
return ( return (
f"Country: {country}\n" "Analyze the attached bestseller product photo, locate its printed artwork, "
f"Pinterest search term: {term}\n" "and produce one ORIGINAL T-shirt print design brief that keeps its general vibe "
f"Reference images attached: {image_count} images.\n\n" "without copying its printed artwork."
f"Analyze the attached images and produce {image_count} ORIGINAL design briefs " )
f"(one per image), each capturing the visual vibe as an original T-shirt print design. "
f"Do NOT copy the images.\n"
f"For EACH brief you MUST set image_index to the 0-based position of the input image " def resolve_pinterest_analyze_system_prompt(country: str = "") -> str:
f"it was derived from (first image = 0, second = 1, ...). Every image_index from 0 to " """多模态分析系统提示词(可配置):命中 prompts/pinterest_analyze_system.md(国家覆盖优先),
f"{max(image_count - 1, 0)} must appear exactly once — this links each brief to its " 否则回退内置 PINTEREST_ANALYZE_SYSTEM_PROMPT。"""
f"source image so the design is generated from the SAME image that was analyzed." text = _pinterest_analyze_prompt_file(country, "pinterest_analyze_system.md")
return text if text else PINTEREST_ANALYZE_SYSTEM_PROMPT
def build_pinterest_analyze_user_prompt(country: str = "") -> str:
"""多模态分析用户提示词(可配置):命中 prompts/pinterest_analyze_user.md 否则内置默认。"""
text = _pinterest_analyze_prompt_file(country, "pinterest_analyze_user.md")
if text:
return text
return (
"Analyze the attached image and produce one ORIGINAL T-shirt print design brief "
"that captures its visual vibe without copying it."
) )
@@ -572,24 +693,31 @@ class OpenAICompatBackend(LLMBackend):
max_used = int((cfg or {}).get("max_used_terms_in_prompt", 100) or 100) max_used = int((cfg or {}).get("max_used_terms_in_prompt", 100) or 100)
if max_used > 0: if max_used > 0:
ctx["used_terms"] = used[-max_used:] ctx["used_terms"] = used[-max_used:]
# 后缀占位符:读 ctx 配置(允许为空=不追加后缀);仅在确有后缀时替换占位
suffix = str(ctx.get("search_term_suffix") or "").strip()
sys_prompt = PINTEREST_TERM_SYSTEM_PROMPT.replace("__SUFFIX__", f" {suffix}" if suffix else "")
user_prompt = build_pinterest_term_user_prompt(ctx).replace("__SUFFIX__", f" {suffix}" if suffix else "")
messages = [ messages = [
{"role": "system", "content": PINTEREST_TERM_SYSTEM_PROMPT}, {"role": "system", "content": sys_prompt},
{"role": "user", "content": build_pinterest_term_user_prompt(ctx)}, {"role": "user", "content": user_prompt},
] ]
raw = _retry(lambda: call_openai_compatible_structured(cfg, messages, PINTEREST_TERM_SCHEMA, timeout=120)) raw = _retry(lambda: call_openai_compatible_structured(cfg, messages, PINTEREST_TERM_SCHEMA, timeout=120))
parsed = _extract_json(raw) parsed = _extract_json(raw)
terms = [str(x).strip() for x in (parsed.get("search_terms", []) or []) if str(x).strip()] terms = [str(x).strip() for x in (parsed.get("search_terms", []) or []) if str(x).strip()]
# 自动追加 " t-shirt design":让 Pinterest 返回真正的 T 恤印花图(更适合作印花设计参考) # 自动追加自定义后缀(config.pinterest.search_term_suffix,默认 t-shirt design):
terms = [f"{t} t-shirt design" if "t-shirt design" not in t.lower() else t for t in terms] # 让 Pinterest 返回真正的印花图(更适合作印花设计参考)
if suffix:
terms = [f"{t} {suffix}" if suffix not in t.lower() else t for t in terms]
return {"search_terms": terms} return {"search_terms": terms}
def analyze_pinterest_images(self, image_paths: List[str], term: str, country: str = "", def analyze_pinterest_images(self, image_paths: List[str], term: str = "", country: str = "",
on_400=None) -> List[Dict[str, Any]]: on_400=None, custom_mode: bool = False) -> List[Dict[str, Any]]:
"""多模态分析 Pinterest 图片 → 原创设计简报列表。 """多模态分析 Pinterest 图片 → 原创设计简报列表。
图片输入不被模型支持(纯文本模型 400)时自动降级纯文本分析(仅用搜索词) 图片输入失败/无有效图片时直接放弃(返回 [],不降级纯文本),由节点跳过该产品
失败返回 [],由节点兜底(回退 mock 规则简报)。
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。 on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
custom_mode: True=自定义模式,用独立的多模态提示词(CUSTOM-REFERENCE 版,
见 prompts/custom_analyze_system.md / custom_analyze_user.md),输入是现有爆款产品图。
""" """
cfg = self._cfg cfg = self._cfg
api_key = cfg.get("api_key", "") api_key = cfg.get("api_key", "")
@@ -605,11 +733,26 @@ class OpenAICompatBackend(LLMBackend):
data_uris: List[str] = [] data_uris: List[str] = []
for p in image_paths: for p in image_paths:
try: try:
pk = Path(p)
raw = pk.read_bytes()
# 校验图片完整性:损坏/截断的图片会被火山方舟等多模态接口直接 400 拒绝,
# 必须滤掉后才能编码 base64(PIL 打开失败即视为损坏)。
if not raw or len(raw) < 100:
print(f"[pinterest_analyze] 图片文件过小/为空,跳过: {p} ({len(raw)}B)")
continue
try:
from PIL import Image
_im = Image.open(pk)
_im.verify() # 校验文件头/结构,不完整则抛异常
_im.close()
except Exception as _ve: # noqa: BLE001
print(f"[pinterest_analyze] 图片损坏/不完整,跳过: {p} ({_ve})")
continue
import base64 as b64 import base64 as b64
mime = "image/png" mime = "image/png"
if Path(p).suffix.lower() in (".jpg", ".jpeg"): if pk.suffix.lower() in (".jpg", ".jpeg"):
mime = "image/jpeg" mime = "image/jpeg"
data_uris.append(f"data:{mime};base64,{b64.b64encode(Path(p).read_bytes()).decode()}") data_uris.append(f"data:{mime};base64,{b64.b64encode(raw).decode()}")
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
print(f"[pinterest_analyze] 图片读取失败 {p}: {e}") print(f"[pinterest_analyze] 图片读取失败 {p}: {e}")
@@ -623,16 +766,21 @@ class OpenAICompatBackend(LLMBackend):
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
pass pass
def _call(use_images: bool) -> str: def _call() -> str:
if custom_mode:
sys_prompt = resolve_custom_analyze_system_prompt()
user_prompt = build_custom_analyze_user_prompt()
else:
sys_prompt = resolve_pinterest_analyze_system_prompt(country)
user_prompt = build_pinterest_analyze_user_prompt(country)
user_content: List[Any] = [ user_content: List[Any] = [
{"type": "text", "text": build_pinterest_analyze_user_prompt(term, country, len(data_uris))}, {"type": "text", "text": user_prompt},
] ]
if use_images: user_content += [{"type": "image_url", "image_url": {"url": u}} for u in data_uris]
user_content += [{"type": "image_url", "image_url": {"url": u}} for u in data_uris]
payload = { payload = {
"model": model, "model": model,
"messages": [ "messages": [
{"role": "system", "content": PINTEREST_ANALYZE_SYSTEM_PROMPT}, {"role": "system", "content": sys_prompt},
{"role": "user", "content": user_content}, {"role": "user", "content": user_content},
], ],
"temperature": 0.5, "temperature": 0.5,
@@ -656,19 +804,15 @@ class OpenAICompatBackend(LLMBackend):
resp.raise_for_status() resp.raise_for_status()
return str(resp.json()["choices"][0]["message"].get("content") or "") return str(resp.json()["choices"][0]["message"].get("content") or "")
raw = "" # 图片输入失败/无有效图片 → 直接放弃该产品(不降级纯文本),由节点跳过后续流程
if data_uris: if not data_uris:
try: print("[pinterest_analyze] 无有效图片输入,放弃该产品(不降级纯文本)")
raw = _call(use_images=True) return []
except Exception as e: # noqa: BLE001 纯文本模型不支持图片 → 降级纯文本 try:
print(f"[pinterest_analyze] 图片输入失败,降级纯文本分析: {e}") raw = _call()
raw = "" except Exception as e: # noqa: BLE001
if not raw: print(f"[pinterest_analyze] 图片分析失败,放弃该产品(不降级纯文本): {e}")
try: return []
raw = _call(use_images=False)
except Exception as e: # noqa: BLE001
print(f"[pinterest_analyze] 分析失败: {e}")
return []
try: try:
parsed = _extract_json(raw) parsed = _extract_json(raw)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
@@ -687,12 +831,7 @@ class OpenAICompatBackend(LLMBackend):
continue continue
designs.append({ designs.append({
"topic": term, "topic": term,
"concept": str(d.get("concept", "")).strip(), "suitable_for_print": bool(d.get("suitable_for_print", True)),
"motif": str(d.get("motif", "")).strip(),
"art_style": str(d.get("art_style", "")).strip(),
"color_palette": str(d.get("color_palette", "")).strip(),
"composition": str(d.get("composition", "")).strip(),
"negative_prompt": str(d.get("negative_prompt", "")).strip(),
"image_prompt": str(d.get("image_prompt", "")).strip(), "image_prompt": str(d.get("image_prompt", "")).strip(),
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应) # 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)
"ref_images": [str(image_paths[i])] if i < len(image_paths) else [], "ref_images": [str(image_paths[i])] if i < len(image_paths) else [],
@@ -703,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 对应模板(缺失回退内置默认)。
模板 1US/GB/AU/MX)返回 {"en_title","cn_title"} 模板 1US/GB/AU/MX)返回 {"en_title","cn_title"}
模板 2(JP)额外返回 {"ja_title"} 模板 2(JP)额外返回 {"ja_title"}
模板 3ES)返回 {"es_title","cn_title"}。 模板 3ES)返回 {"es_title","cn_title"}。
+2
View File
@@ -4,6 +4,7 @@ from .fetch_node import fetch_node
from .filter_node import filter_node from .filter_node import filter_node
from .oss_upload_node import oss_upload_node from .oss_upload_node import oss_upload_node
from .pinterest_analyze_node import pinterest_analyze_node from .pinterest_analyze_node import pinterest_analyze_node
from .pinterest_custom_load_node import pinterest_custom_load_node
from .pinterest_scrape_node import pinterest_scrape_node from .pinterest_scrape_node import pinterest_scrape_node
from .pinterest_search_node import pinterest_search_node from .pinterest_search_node import pinterest_search_node
from .product_node import product_node from .product_node import product_node
@@ -29,4 +30,5 @@ __all__ = [
"pinterest_search_node", "pinterest_search_node",
"pinterest_scrape_node", "pinterest_scrape_node",
"pinterest_analyze_node", "pinterest_analyze_node",
"pinterest_custom_load_node",
] ]
+22 -2
View File
@@ -12,7 +12,7 @@ import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from graph.validate import with_fallback from graph.validate import ThreadSafeErrors, with_fallback
RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"} RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}
@@ -29,6 +29,18 @@ def _notify_400(on_400, exc) -> None:
pass pass
def _notify_503(on_503, exc) -> None:
"""致命图像服务错误(503 / 账户不可用)时触发 on_503 回调(供调用方提前终止任务)。"""
if on_503 is None:
return
try:
from graph.pinterest_pipeline import PinterestPipeline
if PinterestPipeline.is_fatal_503(exc):
on_503()
except Exception: # noqa: BLE001
pass
def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str: def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
lines = [ lines = [
"# POD 印花设计简报(LLM 合规筛选 + 生图提示词)", "# POD 印花设计简报(LLM 合规筛选 + 生图提示词)",
@@ -112,6 +124,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
errors: List[Dict[str, Any]] = None, errors: List[Dict[str, Any]] = None,
seed: Optional[int] = None, seed: Optional[int] = None,
on_400=None, on_400=None,
on_503=None,
size: str = "1024x1024") -> Optional[str]: size: str = "1024x1024") -> Optional[str]:
"""生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。 """生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。
@@ -123,6 +136,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。 无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。 seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。 on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
on_503: 致命图像服务错误(503/账户不可用)时回调(供调用方提前终止任务)。
""" """
try: try:
from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
@@ -145,6 +159,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}") print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}")
_notify_400(on_400, e) _notify_400(on_400, e)
_notify_503(on_503, e)
out_path = ib.generate( out_path = ib.generate(
img_prompt, str(design_dir / f"{out_stem}_design.png"), img_prompt, str(design_dir / f"{out_stem}_design.png"),
brief.get("composite_negative", ""), size=size, seed=seed) brief.get("composite_negative", ""), size=size, seed=seed)
@@ -161,6 +176,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
return out_path return out_path
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
_notify_400(on_400, e) _notify_400(on_400, e)
_notify_503(on_503, e)
if errors is not None: if errors is not None:
errors.append({"node": "compose", "type": type(e).__name__, errors.append({"node": "compose", "type": type(e).__name__,
"message": f"设计稿生成失败 {brief.get('topic','')}: {e}", "trace": ""}) "message": f"设计稿生成失败 {brief.get('topic','')}: {e}", "trace": ""})
@@ -238,12 +254,13 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
def _gen_one(i: int, b: Dict[str, Any]): def _gen_one(i: int, b: Dict[str, Any]):
"""单张设计稿生成(并发线程内调用,每设计一线程)。""" """单张设计稿生成(并发线程内调用,每设计一线程)。"""
out_path = generate_design(ib, b, design_dir, f"{country}_{i:02d}", out_path = generate_design(ib, b, design_dir, f"{country}_{i:02d}",
state.get("errors"), seed=_seed, _safe_errors, seed=_seed,
size=compose_cfg.get("design_size", "1024x1024")) size=compose_cfg.get("design_size", "1024x1024"))
if out_path is None: if out_path is None:
return i, b, None, None return i, b, None, None
return i, b, out_path, None return i, b, out_path, None
_safe_errors = ThreadSafeErrors()
targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)] targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
# 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待 # 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待
workers = max(1, min(len(targets), int((config.get("compose") or {}).get("design_workers", 5)))) workers = max(1, min(len(targets), int((config.get("compose") or {}).get("design_workers", 5))))
@@ -263,6 +280,9 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
b["design_path"] = out_path b["design_path"] = out_path
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path}) designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})
print(f"[compose] 印花设计稿已生成: {out_path}") print(f"[compose] 印花设计稿已生成: {out_path}")
# 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加)
if len(_safe_errors):
state.setdefault("errors", []).extend(list(_safe_errors))
stats = dict(state.get("stats") or {}) stats = dict(state.get("stats") or {})
stats["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)} stats["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)}
+4 -4
View File
@@ -7,6 +7,8 @@
1. 先用种子词走数据源抓取(Google Trends 等),成功即用新数据; 1. 先用种子词走数据源抓取(Google Trends 等),成功即用新数据;
2. 抓取失败/无结果才回退 output/<国>/collected_keywords.json 旧缓存,保证流水线不中断。 2. 抓取失败/无结果才回退 output/<国>/collected_keywords.json 旧缓存,保证流水线不中断。
""" """
import json
from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from graph.sources import get_source from graph.sources import get_source
@@ -44,11 +46,9 @@ def fetch_node(state: Dict[str, Any]) -> Dict[str, Any]:
use_collected = (config.get("fetch") or {}).get("use_collected", True) use_collected = (config.get("fetch") or {}).get("use_collected", True)
if use_collected: if use_collected:
try: try:
import json as _json p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
from pathlib import Path as _Path
p = _Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
if p.exists(): if p.exists():
data = _json.loads(p.read_text(encoding="utf-8")) data = json.loads(p.read_text(encoding="utf-8"))
cached_rows = data.get("keywords") or [] cached_rows = data.get("keywords") or []
if cached_rows: if cached_rows:
rows = [dict(r) for r in cached_rows] # 已过滤去重的关键词 rows = [dict(r) for r in cached_rows] # 已过滤去重的关键词
+7 -7
View File
@@ -5,6 +5,9 @@
2) 真实人物(名单 + Firstname Lastname 模式,仅对 gt_trending 源,避免误删风格词) 2) 真实人物(名单 + Firstname Lastname 模式,仅对 gt_trending 源,避免误删风格词)
3) 设计相关性(剔除泛新闻/科技/赛事词) 3) 设计相关性(剔除泛新闻/科技/赛事词)
""" """
import json
import time
from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from graph.scoring import apply_blacklist, filter_design_relevance, filter_person_names, filter_query_noise from graph.scoring import apply_blacklist, filter_design_relevance, filter_person_names, filter_query_noise
@@ -70,15 +73,13 @@ def filter_node(state: Dict[str, Any]) -> Dict[str, Any]:
# 而不是只显示简报(design_briefs 仅含本次限量生成的热点)。 # 而不是只显示简报(design_briefs 仅含本次限量生成的热点)。
if kept: if kept:
try: try:
import json as _json p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
from pathlib import Path as _Path
p = _Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
p.parent.mkdir(parents=True, exist_ok=True) p.parent.mkdir(parents=True, exist_ok=True)
kws = [{"topic": r.get("topic", ""), "source": r.get("source", ""), kws = [{"topic": r.get("topic", ""), "source": r.get("source", ""),
"kind": r.get("kind", ""), "raw_score": r.get("raw_score")} for r in kept] "kind": r.get("kind", ""), "raw_score": r.get("raw_score")} for r in kept]
p.write_text(_json.dumps({"country": country, p.write_text(json.dumps({"country": country,
"collected_at": _datetime_now(), "collected_at": _datetime_now(),
"keywords": kws}, ensure_ascii=False, indent=2), "keywords": kws}, ensure_ascii=False, indent=2),
encoding="utf-8") encoding="utf-8")
print(f"[filter] 已写入采集缓存 {len(kws)} 条(collected_keywords.json") print(f"[filter] 已写入采集缓存 {len(kws)} 条(collected_keywords.json")
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
@@ -88,5 +89,4 @@ def filter_node(state: Dict[str, Any]) -> Dict[str, Any]:
def _datetime_now() -> str: def _datetime_now() -> str:
import time
return time.strftime("%Y-%m-%dT%H:%M:%S") return time.strftime("%Y-%m-%dT%H:%M:%S")
+65 -80
View File
@@ -3,16 +3,16 @@
图池机制: 图池机制:
- 从持久化图池(image_pool.json)取「未消费」图片(md5 不在 used_images.json)。 - 从持久化图池(image_pool.json)取「未消费」图片(md5 不在 used_images.json)。
- 大图先压缩(内存占用过大 → 缩放/重编码)再送 LLM。 - 大图先压缩(内存占用过大 → 缩放/重编码)再送 LLM。
- 多并发分析(每批 analyze_per_term 张,并发 analyze_concurrency 线程)。 - 多并发分析(每批 1 张,并发 analyze_concurrency 线程;一次 API 请求一张图,利于 LLM 注意力)。
- 每张被分析的图片 md5 一律拉黑(used_images.json)——合适→产出简报→生成设计(设计 md5 全局拉黑见 compose); - 每张被分析的图片 md5 一律拉黑(used_images.json)——合适→产出简报→生成设计(设计 md5 全局拉黑见 compose);
不合适→图片 md5 已拉黑→下一轮自动取下一张,不重复分析。 不合适→图片 md5 已拉黑→下一轮自动取下一张,不重复分析。
- 图池无未消费图片时返回空,由路由触发新一轮搜索。 - 图池无未消费图片时返回空,由路由触发新一轮搜索。
兜底链:LLM 多模态 → 纯文本降级(后端内部)→ mock 规则简报 → 空列表(下游跳过)。 兜底链:LLM 多模态分析失败/无图直接放弃本轮(不降级纯文本、不做 mock 兜底)。
带 with_fallback:任何异常都不中断。 带 with_fallback:任何异常都不中断。
""" """
import concurrent.futures import concurrent.futures
import re import uuid
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
@@ -25,25 +25,14 @@ from graph.pinterest import (
pool_unused_images, pool_unused_images,
save_used_images, save_used_images,
) )
from graph.validate import with_fallback from graph.validate import ThreadSafeErrors, with_fallback
# 明显不适合 T 恤印花的简报主体(启发式过滤;真实判定交给 LLM 搜索词引导)
_BRIEF_UNSUITABLE = re.compile(
r"\b(landscape|panorama|scenery|cityscape|street scene|interior|room decor|"
r"food photography|meal|dinner plate|recipe|makeup|nails|manicure|"
r"weather forecast|map|directions|photorealistic scene|realistic portrait)\b",
re.IGNORECASE,
)
def _brief_suitable(b: Dict[str, Any]) -> bool: def _brief_suitable(b: Dict[str, Any]) -> bool:
"""简报是否适合做 T 恤印花:非侵权(blocked 拦截)+ 有主体 + 非明显非印花概念""" """简报是否适合做 T 恤印花:非侵权(blocked 拦截)+ LLM 判定适合印花(suitable_for_print"""
if str(b.get("risk_level") or "").strip().lower() == "blocked": if str(b.get("risk_level") or "").strip().lower() == "blocked":
return False return False
motif = str(b.get("motif") or "").strip() if not bool(b.get("suitable_for_print", True)):
if not motif:
return False
if _BRIEF_UNSUITABLE.search(motif):
return False return False
return True return True
@@ -75,25 +64,20 @@ def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str,
n = seen_topics.get(base.lower(), 0) n = seen_topics.get(base.lower(), 0)
seen_topics[base.lower()] = n + 1 seen_topics[base.lower()] = n + 1
topic = base if n == 0 else f"{base} #{n + 1}" topic = base if n == 0 else f"{base} #{n + 1}"
motif = str(b.get("motif") or "").strip() or term if not bool(b.get("suitable_for_print", True)):
if not motif: continue # LLM 判定不适合做印花 → 丢弃(md5 已拉黑,下轮取新图)
continue
out.append({ out.append({
"country": country, "country": country,
"topic": topic, "topic": topic,
"risk_level": str(b.get("risk_level") or "safe").strip().lower() or "safe", "risk_level": str(b.get("risk_level") or "safe").strip().lower() or "safe",
"safe_for_print": bool(b.get("safe_for_print", True)), "safe_for_print": bool(b.get("safe_for_print", True)),
"suitable_for_print": bool(b.get("suitable_for_print", True)), "suitable_for_print": True,
"design_category": classify(term), "design_category": classify(term),
"concept": str(b.get("concept") or "").strip() or f"围绕「{term}」的原创印花设计", "concept": f"围绕「{term}」的原创印花设计",
"motif": motif,
"art_style": str(b.get("art_style") or "").strip(),
"color_palette": str(b.get("color_palette") or "").strip(),
"composition": str(b.get("composition") or "").strip(),
"negative_prompt": str(b.get("negative_prompt") or "").strip(),
"image_prompt": str(b.get("image_prompt") or "").strip(), "image_prompt": str(b.get("image_prompt") or "").strip(),
"ref_images": [str(p) for p in (b.get("ref_images") or []) if str(p)], "ref_images": [str(p) for p in (b.get("ref_images") or []) if str(p)],
"source_md5": str(b.get("source_md5") or "").strip().lower(), "source_md5": str(b.get("source_md5") or "").strip().lower(),
"brief_id": str(b.get("brief_id") or "").strip(),
"slogan": "", "slogan": "",
"score": 1.0, "score": 1.0,
"confidence": 1.0, "confidence": 1.0,
@@ -110,23 +94,30 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
errors = list(state.get("errors") or []) errors = list(state.get("errors") or [])
pcfg = config.get("pinterest") or {} pcfg = config.get("pinterest") or {}
analyze_per_term = int(pcfg.get("analyze_per_term", 1)) analyze_per_term = 1 # 固定一次 API 请求分析 1 张图(利于 LLM 注意力;不再用 analyze_per_term 配置)
concurrency = int(pcfg.get("analyze_concurrency", 3)) concurrency = int(pcfg.get("analyze_concurrency", 3))
max_designs = int(pcfg.get("max_designs", 10)) max_designs = int(pcfg.get("max_designs", 10))
n_ref = max(1, int(pcfg.get("ref_images_per_design", 1))) n_ref = max(1, int(pcfg.get("ref_images_per_design", 1)))
provider = str(pcfg.get("provider") or "openai").strip().lower() provider = str(pcfg.get("provider") or "openai").strip().lower()
# 按需分析:只取补齐到目标所需的图片数(batch_size 为上限,不超额分析),并按 md5 去重, # 简报池还有待处理/在途简报 → 不分析新图(等后台消化完再由路由决定下一步)
# 保证同一图片内容(md5)不会同时被多条简报使用 pipe = state.get("pinterest_pipeline")
target = int(state.get("pinterest_target") or 0) if pipe is not None and hasattr(pipe, "pending_count") and pipe.pending_count() > 0:
existing = state.get("briefs") or [] print(f"[pinterest_analyze] 简报池还有 {pipe.pending_count()} 条在途,本轮不分析")
remaining = max(0, target - len(existing)) return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
"errors": errors}
# 按需分析:一次把图池当前未消费图片全部分析成简报(搜集完一批→生成一批简报),
# 不再按目标任务数截断(不够用了才由路由触发新采集)。analyze_batch 可设上限保护配额。
batch_size = int(pcfg.get("analyze_batch", 0)) batch_size = int(pcfg.get("analyze_batch", 0))
if batch_size <= 0: if batch_size <= 0:
# 自动:一次分析补齐到「目标所需」或「每词简报上限」的较小值(每张图→1条简报) batch_size = max_designs # 自动:一次最多分析 max_designs 张(每张图→1条简报)
# 让 pipeline 队列一次有足够任务,时刻保持并发生成(避免每轮只推 6 条导致线程空转) need = batch_size
batch_size = min(remaining, max_designs) # 按目标简报数收敛:最多分析到满足选品清单所需的简报数即可(target=spu_tasks,每款一个设计),
need = min(batch_size, remaining) if remaining > 0 else 0 # 普通与自定义模式一致——避免单一产品任务也把整池未消费图片全部分析成冗余简报、浪费配额;
# 分析出的简报若不适合可丢弃,缺口不足时再由路由补分析/search。
_t = int(state.get("pinterest_target") or 1) or 1
need = max(1, min(need, _t))
# 1) 图池取未消费图片(md5 不在 used_images);无 → 返回空,路由触发搜索 # 1) 图池取未消费图片(md5 不在 used_images);无 → 返回空,路由触发搜索
pool = load_image_pool(output_dir, country) pool = load_image_pool(output_dir, country)
@@ -137,10 +128,6 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
return {"pinterest_briefs": [], "briefs": state.get("briefs") or [], return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
"errors": errors} "errors": errors}
if need <= 0:
print("[pinterest_analyze] 简报已达标,无需分析")
return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
"errors": errors}
seen_md5: set = set() seen_md5: set = set()
batch: List[Dict[str, Any]] = [] batch: List[Dict[str, Any]] = []
for img in unused: for img in unused:
@@ -148,34 +135,27 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
if m and m in seen_md5: if m and m in seen_md5:
continue # 同一图片内容(md5)不重复分析 continue # 同一图片内容(md5)不重复分析
seen_md5.add(m) seen_md5.add(m)
img["brief_id"] = str(uuid.uuid4()) # md5 校验后分配全局唯一 id,用于图-简报对应校验
batch.append(img) batch.append(img)
if len(batch) >= need: if len(batch) >= need:
break break
print(f"[pinterest_analyze] 图池取 {len(batch)} 张未消费图片分析(按需 {need}" print(f"[pinterest_analyze] 图池取 {len(batch)} 张未消费图片分析(本批上限 {need}"
f"池剩余未消费 {len(unused) - len(batch)} 张,已消费 {len(used)} 张)") f"池剩余未消费 {len(unused) - len(batch)} 张,已消费 {len(used)} 张)")
def _assign_refs(res: List[Dict[str, Any]], chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _assign_refs(res: List[Dict[str, Any]], chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""按简报的 image_index(LLM 返回)匹配它实际分析的图,写入 source_md5 + ref_images。 """每批只分析 1 张图:简报按顺序对应 chunk 里唯一一张图,写入 source_md5 + ref_images + brief_id
全局 id 校验:简报必须带 image_index(对应输入第几张图,0-based); 全局 id 校验:分析前已给每张图分配 uuid4(brief_id),简报回填同一 brief_id
无 image_indexmock 兜底)→ 回退按顺序;无效/越界/重复 → 丢弃该简报(避免错位)。 保证图-简报严格对应(一次 API 请求一张图,无 image_index 错位问题)。
这样 analyze_per_term 可 >1 一次分析多张图提速,简报仍严格对应各自的图。
""" """
chunk_paths = [img["path"] for img in chunk] chunk_paths = [img["path"] for img in chunk]
chunk_md5s = [str(img.get("md5") or "").strip().lower() for img in chunk] chunk_md5s = [str(img.get("md5") or "").strip().lower() for img in chunk]
used_idx: set = set() chunk_ids = [str(img.get("brief_id") or "") for img in chunk]
out: List[Dict[str, Any]] = [] out: List[Dict[str, Any]] = []
for i, b in enumerate(res): for i, b in enumerate(res):
if not isinstance(b, dict): if not isinstance(b, dict):
continue continue
try: idx = i if i < len(chunk_paths) else 0 # 一次一张:简报按顺序对应唯一图
idx = int(b.get("image_index"))
except (TypeError, ValueError):
idx = i # 无 image_index → 回退按顺序
if idx < 0 or idx >= len(chunk_paths) or idx in used_idx:
print(f"[pinterest_analyze] 简报 image_index={idx} 无效/重复,丢弃(避免图-简报错位)")
continue
used_idx.add(idx)
refs: List[str] = [] refs: List[str] = []
for k in range(n_ref): for k in range(n_ref):
src = chunk_paths[(idx + k) % len(chunk_paths)] src = chunk_paths[(idx + k) % len(chunk_paths)]
@@ -183,6 +163,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
refs.append(src) refs.append(src)
b["ref_images"] = refs b["ref_images"] = refs
b["source_md5"] = chunk_md5s[idx] if idx < len(chunk_md5s) else "" b["source_md5"] = chunk_md5s[idx] if idx < len(chunk_md5s) else ""
b["brief_id"] = chunk_ids[idx] if idx < len(chunk_ids) else ""
out.append(b) out.append(b)
return out return out
@@ -210,6 +191,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
# 4) 多并发分析(每线程分析一个 chunk;LLM 后端只读 self._cfg,线程安全) # 4) 多并发分析(每线程分析一个 chunk;LLM 后端只读 self._cfg,线程安全)
raw_briefs: List[Dict[str, Any]] = [] raw_briefs: List[Dict[str, Any]] = []
_safe_errors = ThreadSafeErrors()
def _analyze_chunk(chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _analyze_chunk(chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
term = str(chunk[0].get("term") or "") term = str(chunk[0].get("term") or "")
@@ -221,37 +203,32 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
if pipe is not None and hasattr(pipe, "record_400"): if pipe is not None and hasattr(pipe, "record_400"):
if pipe.record_400(): if pipe.record_400():
pipe._abort_current_term() pipe._abort_current_term()
res = llm.analyze_pinterest_images(paths, term, country, on_400=_on_400) or [] res = llm.analyze_pinterest_images(paths, term, country, on_400=_on_400,
custom_mode=bool(state.get("custom_mode"))) or []
# 把每条简报的来源图路径回填为原始图(压缩图仅用于分析,参考图用原图) # 把每条简报的来源图路径回填为原始图(压缩图仅用于分析,参考图用原图)
return _assign_refs(res, chunk) return _assign_refs(res, chunk)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
errors.append({"node": "pinterest_analyze", "type": type(e).__name__, _safe_errors.append({"node": "pinterest_analyze", "type": type(e).__name__,
"message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""}) "message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""})
print(f"[pinterest_analyze] 「{term}」分析失败: {e}") print(f"[pinterest_analyze] 「{term}」分析失败: {e}")
return [] return []
return [] return []
workers = max(1, min(concurrency, len(chunks))) workers = max(1, min(concurrency, len(chunks)))
if len(chunks) > 1: if len(chunks) > 1:
print(f"[pinterest_analyze] 并发分析 {len(chunks)} 批({workers} 线程,每批 {analyze_per_term} 张)…") print(f"[pinterest_analyze] 并发分析 {len(chunks)} 批({workers} 线程,每批 1 张)…")
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as _ex: with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as _ex:
_futs = [_ex.submit(_analyze_chunk, c) for c in chunks] _futs = [_ex.submit(_analyze_chunk, c) for c in chunks]
for _f in concurrent.futures.as_completed(_futs): for _f in concurrent.futures.as_completed(_futs):
raw_briefs.extend(_f.result()) raw_briefs.extend(_f.result())
# 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加)
if len(_safe_errors):
errors.extend(list(_safe_errors))
# 5) 兜底:LLM 无结果 → mock 规则简报(零 API 成本,保证有设计可生成) # 5) 真实 LLM 图片分析无结果(失败/无有效图片)→ 直接放弃本轮,跳过后续流程
# 不再 mock 兜底生成设计(用户要求:分析失败即放弃该产品)
if not raw_briefs: if not raw_briefs:
try: print("[pinterest_analyze] 图片分析无结果,放弃本轮简报(跳过后续流程)")
mock = get_backend("mock")
for chunk in chunks:
term = str(chunk[0].get("term") or "")
paths = [compressed_map.get(img["path"], img["path"]) for img in chunk]
res = mock.analyze_pinterest_images(paths, term, country) or []
_assign_refs(res, chunk)
raw_briefs.extend(res)
print(f"[pinterest_analyze] 兜底:mock 规则简报 {len(raw_briefs)}")
except Exception as e: # noqa: BLE001
print(f"[pinterest_analyze] mock 兜底失败: {e}")
# 6) 本批所有图片 md5 一律拉黑(已消费,不再复用)——合适/不合适都拉黑 # 6) 本批所有图片 md5 一律拉黑(已消费,不再复用)——合适/不合适都拉黑
for img in batch: for img in batch:
@@ -267,19 +244,20 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
if len(kept) < len(raw_briefs): if len(kept) < len(raw_briefs):
print(f"[pinterest_analyze] 简报过滤:{len(raw_briefs)}{len(kept)} 条适合印花") print(f"[pinterest_analyze] 简报过滤:{len(raw_briefs)}{len(kept)} 条适合印花")
# 8) 上限 + 去重(同 motif+style 指纹只留一条 # 8) 上限 + 去重(同 brief_id 只留一条;brief_id 为每张图 uuid4,天然唯一
kept = kept[:max_designs] kept = kept[:max_designs]
seen: set = set() seen: set = set()
uniq: List[Dict[str, Any]] = [] uniq: List[Dict[str, Any]] = []
for b in kept: for b in kept:
fp = f"{str(b.get('motif', '')).strip().lower()}|{str(b.get('art_style', '')).strip().lower()}" fp = str(b.get("brief_id") or b.get("image_prompt") or "").strip().lower()
if fp in seen: if not fp or fp in seen:
continue continue
seen.add(fp) seen.add(fp)
uniq.append(b) uniq.append(b)
kept = uniq kept = uniq
# 9) 富化 → screened → prompt_node 装配提示词 → 标准 briefs(追加到累计,按需截断到目标数 # 9) 富化 → screened → prompt_node 装配提示词 → 标准 briefs(追加到累计)
# 不再按目标任务数截断:搜集完一批→生成一批简报,不够用了才由路由触发新采集
existing_topics = [str(b.get("topic", "")).strip() for b in (state.get("briefs") or [])] existing_topics = [str(b.get("topic", "")).strip() for b in (state.get("briefs") or [])]
screened = _enrich_briefs(kept, country, existing_topics) screened = _enrich_briefs(kept, country, existing_topics)
new_briefs: List[Dict[str, Any]] = [] new_briefs: List[Dict[str, Any]] = []
@@ -290,10 +268,16 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
old_count = len(state.get("briefs") or []) old_count = len(state.get("briefs") or [])
accumulated = list(state.get("briefs") or []) accumulated = list(state.get("briefs") or [])
accumulated.extend(new_briefs) accumulated.extend(new_briefs)
target = int(state.get("pinterest_target") or 0)
if target > 0 and len(accumulated) > target: # 连续无产出计数器:本次无新增简报 → 递增(防"逐一取下一张参考图"空转);
accumulated = accumulated[:target] # 有新增简报 → 重置为 0
print(f"[pinterest_analyze] 简报已达目标 {target} 条,截断多余部分") empty_rounds = int(state.get("pinterest_empty_rounds") or 0)
if new_briefs:
empty_rounds = 0
else:
empty_rounds += 1
print(f"[pinterest_analyze] 本轮无新增简报(连续 {empty_rounds} 轮无产出),"
f"超过上限后将自动结束")
# 推送实际新增且保留的简报到并发生成流水线(简报池):边分析边生成设计/三合一/种草图 # 推送实际新增且保留的简报到并发生成流水线(简报池):边分析边生成设计/三合一/种草图
pushed = accumulated[old_count:] pushed = accumulated[old_count:]
@@ -319,4 +303,5 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
} }
print(f"[pinterest_analyze] 本轮分析 {len(batch)} 张图 → 简报 {len(new_briefs)} 条," print(f"[pinterest_analyze] 本轮分析 {len(batch)} 张图 → 简报 {len(new_briefs)} 条,"
f"累计 {len(accumulated)} 条({country}") f"累计 {len(accumulated)} 条({country}")
return {"pinterest_briefs": kept, "briefs": accumulated, "stats": stats, "errors": errors} return {"pinterest_briefs": kept, "briefs": accumulated, "stats": stats, "errors": errors,
"pinterest_empty_rounds": empty_rounds}
+82
View File
@@ -0,0 +1,82 @@
"""Pinterest 参考模式自定义节点:加载本地图片文件夹 → 校验数量 → 注册进图池(pinterest_custom_load)。
仅自定义模式(pinterest.mode=custom)使用,替代 pinterest_search + pinterest_scrape
直接把 pinterest.custom_image_dir 内的有效图片(jpg/jpeg/png/webp)注册进图池,
再复用 pinterest_analyze 直接送多模态分析,沿用 Pinterest 后续所有步骤
(分析→设计→三合一→OSS→种草图→模板导出)。
数量校验(硬校验,不满足则不启动分析):
1. 文件夹必须有有效图片(>0);
2. 选品清单总数(product.spu_tasks 展开后,state.pinterest_target)必须 ≤ 有效图片数 ——
「选品清单不得大于有效图片数」。
每次运行把图池重建为该文件夹的图片集(自定义模式唯一图源),并清空已消费拉黑
used_images),保证用户每次重新上传/选择文件夹的所有有效图片都会被重新多模态分析。
"""
from typing import Any, Dict
from graph.pinterest import (
image_md5,
list_valid_images,
save_image_pool,
save_used_images,
)
from graph.validate import with_fallback
@with_fallback("pinterest_custom_load")
def pinterest_custom_load_node(state: Dict[str, Any]) -> Dict[str, Any]:
config = state["config"]
output_dir = state["output_dir"]
country = state["country"]
pcfg = config.get("pinterest") or {}
folder = str(pcfg.get("custom_image_dir") or "").strip()
images = list_valid_images(folder)
valid_n = len(images)
target = int(state.get("pinterest_target") or 1)
errors = list(state.get("errors") or [])
stats = dict(state.get("stats") or {})
# —— 硬校验 1:必须有有效图片 ——
if valid_n == 0:
msg = (f"自定义图片文件夹「{folder or '(未填写)'}」中没有有效图片"
f"(请填写 pinterest.custom_image_dir,文件夹内应有 jpg/jpeg/png/webp 图片)")
errors.append({"node": "pinterest_custom_load", "type": "ValidationError",
"message": msg, "trace": ""})
stats["pinterest_custom"] = {"folder": folder, "valid_images": 0, "target": target, "ok": False}
print(f"[pinterest_custom_load] ❌ {msg}")
return {"errors": errors, "stats": stats}
# —— 硬校验 2:选品清单总数 ≤ 有效图片数 ——
if target > valid_n:
msg = (f"选品清单数量({target})大于自定义图片有效数量({valid_n}):"
f"选品清单不得大于有效图片数,请补充图片或减少选品")
errors.append({"node": "pinterest_custom_load", "type": "ValidationError",
"message": msg, "trace": ""})
stats["pinterest_custom"] = {"folder": folder, "valid_images": valid_n, "target": target, "ok": False}
print(f"[pinterest_custom_load] ❌ {msg}")
return {"errors": errors, "stats": stats}
# —— 每次运行重建图池为该文件夹图片集 + 清空已消费拉黑 → 所有有效图片都被重新分析 ——
pool = {"updated_at": "", "images": []}
seen_md5: set = set()
for f in images:
m = str(image_md5(str(f)) or "").strip().lower()
if not m or m in seen_md5:
continue
seen_md5.add(m)
pool["images"].append({"path": str(f), "md5": m, "term": "custom"})
save_image_pool(output_dir, country, pool)
save_used_images(output_dir, country, set())
stats["pinterest_custom"] = {
"folder": folder, "valid_images": valid_n,
"target": target, "loaded": len(pool["images"]), "ok": True,
}
print(f"[pinterest_custom_load] 自定义图源:{folder} → 有效图片 {valid_n} 张(选品清单 {target}),"
f"已注册进图池,直接进入多模态分析")
return {"custom_mode": True, "pinterest_custom_folder": folder,
"custom_load_ok": True, "errors": errors, "stats": stats}
+31 -3
View File
@@ -55,6 +55,31 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
headless = bool(pcfg.get("headless", False)) headless = bool(pcfg.get("headless", False))
proxy = pcfg.get("proxy") or None proxy = pcfg.get("proxy") or None
search_mode = str(pcfg.get("search_mode") or "direct").strip().lower() search_mode = str(pcfg.get("search_mode") or "direct").strip().lower()
login_check = bool(pcfg.get("login_check", True))
login_wait = bool(pcfg.get("login_wait", False))
# 爬取前静态检测 Pinterest 登录态(不启动 Chrome,只读 .chrome_session cookies):
# 未登录/无会话 → 跳过本轮爬取并告警,避免每个搜索词都启动 Chrome 后才发现未登录。
login_state: Dict[str, Any] = {"status": "unknown"}
if login_check:
try:
from pinterest_scraper.pinterest_image_capture import check_login_state
login_state = check_login_state()
except Exception as e: # noqa: BLE001
login_state = {"status": "unknown", "detail": f"登录态检测失败: {e}"}
status = login_state.get("status")
if status in ("logged_out", "no_session"):
print(f"[pinterest_scrape] ⚠️ 未检测到 Pinterest 登录态({login_state.get('detail')})。"
f"跳过本轮 {len(terms)} 个搜索词爬取。")
stats = dict(state.get("stats") or {})
stats["pinterest_scrape"] = {
"terms": len(terms), "scraped": 0, "skipped": 0, "failed": 0,
"images": 0, "pool": len((load_image_pool(output_dir, country).get("images")) or []),
"login_status": status,
}
return {"pinterest_images": {}, "pinterest_attempted": state.get("pinterest_attempted") or [],
"pinterest_login": login_state, "stats": stats, "errors": errors}
print(f"[pinterest_scrape] 登录态检测:{status}{login_state.get('detail')}")
# 所有搜索词共享同一个 .chrome_session 登录态目录,Chrome 对同一 user-data-dir 是单例, # 所有搜索词共享同一个 .chrome_session 登录态目录,Chrome 对同一 user-data-dir 是单例,
# 并发启动会互相抢占导致 "browser has been closed",必须串行爬取。 # 并发启动会互相抢占导致 "browser has been closed",必须串行爬取。
@@ -62,7 +87,8 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
print(f"[pinterest_scrape] 共享登录态目录不支持并发,scrape_concurrency 强制为 1(原 {concurrency}") print(f"[pinterest_scrape] 共享登录态目录不支持并发,scrape_concurrency 强制为 1(原 {concurrency}")
concurrency = 1 concurrency = 1
# 一次性探测并校验代理(Pinterest 需代理才能访问;代理失效时给出明确警告,避免逐词静默失败) # 默认走代理:config.pinterest.proxy 未配置时自动探测(环境变量/系统代理/本地常见端口),
# 本地 VPN 已开启时探测到的代理即可访问 Pinterest;代理失效时给出明确警告,避免逐词静默失败。
if proxy is None: if proxy is None:
try: try:
from pinterest_scraper.pinterest_image_capture import detect_proxy, get_system_proxy, _validate_proxy from pinterest_scraper.pinterest_image_capture import detect_proxy, get_system_proxy, _validate_proxy
@@ -91,7 +117,8 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
try: try:
from pinterest_scraper.scraper import scrape_pinterest from pinterest_scraper.scraper import scrape_pinterest
files = scrape_pinterest(term, count=images_per_term, files = scrape_pinterest(term, count=images_per_term,
save_dir=str(term_dir), proxy=proxy, headless=headless) save_dir=str(term_dir), proxy=proxy, headless=headless,
login_wait=login_wait)
results[term] = files results[term] = files
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
failed.append(term) failed.append(term)
@@ -150,8 +177,9 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
stats["pinterest_scrape"] = { stats["pinterest_scrape"] = {
"terms": len(terms), "scraped": len(results), "skipped": len(skipped), "terms": len(terms), "scraped": len(results), "skipped": len(skipped),
"failed": len(failed), "images": total, "pool": len(pool.get("images") or []), "failed": len(failed), "images": total, "pool": len(pool.get("images") or []),
"login_status": login_state.get("status", "unknown"),
} }
print(f"[pinterest_scrape] 完成:{len(results)} 个搜索词,共 {total} 张图(跳过 {len(skipped)},失败 {len(failed)}") print(f"[pinterest_scrape] 完成:{len(results)} 个搜索词,共 {total} 张图(跳过 {len(skipped)},失败 {len(failed)}")
return {"pinterest_images": results, "pinterest_attempted": attempted, return {"pinterest_images": results, "pinterest_attempted": attempted,
"stats": stats, "errors": errors} "pinterest_login": login_state, "stats": stats, "errors": errors}
+7 -4
View File
@@ -33,6 +33,8 @@ def pinterest_search_node(state: Dict[str, Any]) -> Dict[str, Any]:
provider = str(pcfg.get("provider") or "openai").strip().lower() provider = str(pcfg.get("provider") or "openai").strip().lower()
search_mode = str(pcfg.get("search_mode") or "direct").strip().lower() search_mode = str(pcfg.get("search_mode") or "direct").strip().lower()
_raw_suffix = str(pcfg.get("search_term_suffix") if pcfg.get("search_term_suffix") is not None else " t-shirt design").strip()
suffix = _raw_suffix if _raw_suffix else "" # 配置留空则后缀为空(不追加),不再回退默认
want = int(pcfg.get("search_terms_per_run", 1)) # 每次搜索词数量 want = int(pcfg.get("search_terms_per_run", 1)) # 每次搜索词数量
seed_sample = int(pcfg.get("seed_sample", 40)) seed_sample = int(pcfg.get("seed_sample", 40))
max_used_in_prompt = int(pcfg.get("max_used_terms_in_prompt", 100)) max_used_in_prompt = int(pcfg.get("max_used_terms_in_prompt", 100))
@@ -57,9 +59,9 @@ def pinterest_search_node(state: Dict[str, Any]) -> Dict[str, Any]:
fresh = [s for s in pool if s.lower() not in used_set] fresh = [s for s in pool if s.lower() not in used_set]
if not fresh: if not fresh:
fresh = pool # 库内词全部用过 → 允许复用(词库有限) fresh = pool # 库内词全部用过 → 允许复用(词库有限)
terms = [f"{s} t-shirt design" if "t-shirt design" not in s.lower() else s terms = [f"{s} {suffix}".strip() if suffix and suffix not in s.lower() else s
for s in random.sample(fresh, min(want, len(fresh)))] for s in random.sample(fresh, min(want, len(fresh)))]
print(f"[pinterest_search] direct 模式:国家种子词库随机抽 {len(terms)} 个 + t-shirt design{country}") print(f"[pinterest_search] direct 模式:国家种子词库随机抽 {len(terms)} 个 + 后缀「{suffix}{country}")
else: else:
used_llm = merge_used(used, attempted) used_llm = merge_used(used, attempted)
if max_used_in_prompt > 0: if max_used_in_prompt > 0:
@@ -79,7 +81,8 @@ def pinterest_search_node(state: Dict[str, Any]) -> Dict[str, Any]:
if llm is not None and hasattr(llm, "generate_pinterest_terms"): if llm is not None and hasattr(llm, "generate_pinterest_terms"):
try: try:
ctx = {"country": country, "seeds": seeds, "used_terms": used_llm, "count": want} ctx = {"country": country, "seeds": seeds, "used_terms": used_llm, "count": want,
"search_term_suffix": suffix}
res = llm.generate_pinterest_terms(ctx) res = llm.generate_pinterest_terms(ctx)
terms = [str(t).strip() for t in (res.get("search_terms") or []) if str(t).strip()] terms = [str(t).strip() for t in (res.get("search_terms") or []) if str(t).strip()]
print(f"[pinterest_search] LLM 生成搜索词 {len(terms)} 个({country},已用词注入 {len(used_llm)}") print(f"[pinterest_search] LLM 生成搜索词 {len(terms)} 个({country},已用词注入 {len(used_llm)}")
@@ -89,7 +92,7 @@ def pinterest_search_node(state: Dict[str, Any]) -> Dict[str, Any]:
# 3) 兜底:LLM 无结果 → 种子词池随机抽样 # 3) 兜底:LLM 无结果 → 种子词池随机抽样
if not terms: if not terms:
terms = [f"{s} t-shirt design" if "t-shirt design" not in s.lower() else s terms = [f"{s} {suffix}".strip() if suffix and suffix not in s.lower() else s
for s in random.sample(seeds, min(want, len(seeds)))] for s in random.sample(seeds, min(want, len(seeds)))]
print(f"[pinterest_search] 兜底:从种子词池取 {len(terms)}") print(f"[pinterest_search] 兜底:从种子词池取 {len(terms)}")
+176 -61
View File
@@ -34,22 +34,18 @@ from typing import Any, Dict, List, Optional
from graph.paths import project_root, runtime_root from graph.paths import project_root, runtime_root
from graph.product import ( from graph.product import (
find_basemap, find_basemap,
find_first_model_folder,
first_available_sku, first_available_sku,
list_colors, list_colors,
list_spus, list_spus,
) )
from graph.validate import with_fallback from graph.validate import ThreadSafeErrors, with_fallback
_USED_LOCK = threading.Lock() # used_designs.json 并发写锁 _USED_LOCK = threading.Lock() # used_designs.json 并发写锁
_MODEL_LOCK = threading.Lock() # 同款共用模特缓存并发锁
_MODEL_CACHE: Dict[str, Any] = {} # 同款共用模特:spu_code → model 路径
def _next_img_idx(prod_dir: Path, prefix: str) -> int: def _next_img_idx(prod_dir: Path, prefix: str) -> int:
"""货号续号:扫 prod_dir 已有 {prefix}{数字}* 文件,返回下一个起始序号(不覆盖旧产物)。""" """货号续号:扫 prod_dir 已有 {prefix}{数字}* 文件,返回下一个起始序号(不覆盖旧产物)。"""
import re
max_n = -1 max_n = -1
try: try:
if prod_dir.exists(): if prod_dir.exists():
@@ -87,6 +83,57 @@ MODEL_WEAR_PROMPT = (
"图1原本的背景、人物、构图及光影结构100%不变,仅替换图1衣服上的印花图案与衣服底色。" "图1原本的背景、人物、构图及光影结构100%不变,仅替换图1衣服上的印花图案与衣服底色。"
) )
# 平铺图专属三图合成提示词(mark 配置 flat_dir 平铺图文件夹):图1平铺实拍 + 图2印花 + 图3底图
FLAT_LAY_PROMPT = (
"你是一个专业的电商AI视觉合成工具,执行“高保真印花与色彩移植/印花替换”:把图2的印花设计"
"印到图3底色的面料上,替换图1平铺衣服原有的底色与图案,输出一张“图3底色+图2印花”的平铺服装商品图。"
"全程无人参与。\n"
"【图片角色,按提交顺序】\n"
"图1=衣服平铺实拍图(基底图:提供衣服版型、轮廓、褶皱、光影、拍摄背景与构图,"
"最终输出必须与图1同角度、同摆放);\n"
"图2=纯印花设计稿;\n"
"图3=平铺衣服底图(只取衣服本身的底色与面料材质,忽略平铺图背景/桌面/场景,只保留面料颜色与质感)。\n"
"TASK: 把图2的印花设计印到图3底色的面料上,替换图1平铺衣服原有的底色与图案,"
"输出一张“图3底色+图2印花”的平铺服装商品图。全程无人参与。\n"
"【执行规则】\n"
"0.禁止人物:输出图中严禁出现任何人体、模特、人台/假人、头颈、手臂或“穿着效果”,"
"必须保持图1的纯平铺俯拍商品图形式,衣服平放在原背景上。\n"
"1.底色锁定:从图3提取衣服底色与面料,最终合成中必须100%保持不变,严禁偏色。\n"
"2.印花提取:从图2精准提取纯印花图案(线条/色号/比例),叠加到图3底色上形成合成面料。\n"
"3.印花尺寸适配:印花整体尺寸与衣服面料面积成合理比例,居中印在胸/背/衣身主体区域,"
"占衣身面积约30%-45%,四周留白,严禁过大撑满整件或过小(低于20%)。\n"
"4.主体遮罩:识别图1中衣服本体的完整区域(领口到下摆、含袖子;忽略背景/桌面/无关物品),"
"用合成面料完整覆盖,彻底清除原衣服的颜色与图案;图1中衣服以外的物品保持原样。\n"
"5.精准贴合:合成面料严格跟随图1衣服的平铺形态,领口/袖口/下摆/侧缝等版型结构清晰保留,"
"褶皱/翻折/堆叠处印花随之自然变形,杜绝“贴纸感”与“平面涂色感”。\n"
"6.光影融合:按图1拍摄光线方向调整亮度/对比度,印花随褶皱产生明暗变化但色号不偏移;"
"衣服投影与图1保持一致。\n"
"7.纯净输出:仅输出一张最终合成平铺图;图1的背景/桌面/构图/角度/光影100%不变,"
"仅替换衣服的底色与印花;画面中不得出现任何人像、肢体或人台。"
)
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" → 模特三图提示词(优先读 prompts/model_prompt.md,缺失/留空回退内置 MODEL_WEAR_PROMPT);
kind="flat" → 平铺三图提示词(优先读 prompts/flat_prompt.md,缺失/留空回退内置 FLAT_LAY_PROMPT)。
"""
if kind == "flat":
return _read_prompt_md("flat_prompt.md") or FLAT_LAY_PROMPT
return _read_prompt_md("model_prompt.md") 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]:
"""选定 SKU:显式指定优先;否则第一个有本地底图的;再无则第一个颜色(便于模板导出)。""" """选定 SKU:显式指定优先;否则第一个有本地底图的;再无则第一个颜色(便于模板导出)。"""
@@ -116,17 +163,30 @@ def _template_out_path(prod_dir: Path, chosen_sku: str) -> Path:
return prod_dir / f"{chosen_sku}_已填写_{int(time.time())}.xlsx" return prod_dir / f"{chosen_sku}_已填写_{int(time.time())}.xlsx"
def _is_fatal_50x(e) -> bool:
"""致命图像服务错误(503 / No available compatible accounts)→ 不重试,提前终止。"""
try:
from graph.pinterest_pipeline import PinterestPipeline
return PinterestPipeline.is_fatal_503(e)
except Exception: # noqa: BLE001
return False
def _retry_image(fn, *args, attempts: int = 3, backoff=(5, 20, 40), **kwargs): def _retry_image(fn, *args, attempts: int = 3, backoff=(5, 20, 40), **kwargs):
"""图像合成带退避重试(网关超载/超时常见):成功返回 out_path;全部失败返回 None。""" """图像合成带退避重试(网关超载/超时常见):成功返回 out_path;全部失败返回 None。
import time as _t
致命 503(账户不可用)重试无效 → 直接抛出,交由调用方终止任务。
"""
last = None last = None
for i in range(attempts): for i in range(attempts):
try: try:
return fn(*args, **kwargs) return fn(*args, **kwargs)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
if _is_fatal_50x(e):
raise
last = e last = e
if i < attempts - 1: if i < attempts - 1:
_t.sleep(backoff[i]) time.sleep(backoff[i])
print(f"[product] 图像合成重试 {attempts} 次均失败: {last}") print(f"[product] 图像合成重试 {attempts} 次均失败: {last}")
return None return None
@@ -135,16 +195,32 @@ 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",
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。 """处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。 shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
img_code: 货号(前缀+3位计数);本产品所有图片文件归入 prod_dir/{img_code}/ 子文件夹 img_code: 货号(前缀+3位计数);本产品所有图片文件归入 prod_dir/{img_code}/ 子文件夹
(按货号命名,包含该货号对应的所有图片)。 (按货号命名,包含该货号对应的所有图片)。
on_503: 致命图像服务错误(503/账户不可用)回调(供调用方提前终止任务)。
model_kind: 图源类型 "model"(模特图)/ "flat"(平铺图),决定用哪个合成提示词。
返回 result dict;内部异常已兜底,不中断。 返回 result dict;内部异常已兜底,不中断。
""" """
# 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里 # 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里
prod_dir = prod_dir / img_code prod_dir = prod_dir / img_code
prod_dir.mkdir(parents=True, exist_ok=True) prod_dir.mkdir(parents=True, exist_ok=True)
def _fatal(e) -> bool:
"""致命图像服务错误(503/账户不可用)→ 通知 on_503 并返回 True(调用方应立即终止)。"""
try:
from graph.pinterest_pipeline import PinterestPipeline
if PinterestPipeline.is_fatal_503(e):
if on_503 is not None:
on_503()
return True
except Exception: # noqa: BLE001
pass
return False
colors = list_colors(db_path, spu["code"]) colors = list_colors(db_path, spu["code"])
valid_codes = {c["sku_code"] for c in colors} valid_codes = {c["sku_code"] for c in colors}
if sku_code: if sku_code:
@@ -220,59 +296,74 @@ def _process_spu(
result["design_from"] = "product" result["design_from"] = "product"
print(f"{tag} 纯印花设计稿已生成(product 节点): {design_path}") print(f"{tag} 纯印花设计稿已生成(product 节点): {design_path}")
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
if _fatal(e):
print(f"{tag} 图像服务 503,终止: {e}")
return None
errors.append({"node": "product", "type": type(e).__name__, "message": f"设计稿生成失败: {e}", "trace": ""}) errors.append({"node": "product", "type": type(e).__name__, "message": f"设计稿生成失败: {e}", "trace": ""})
print(f"{tag} 设计稿生成失败: {e}") print(f"{tag} 设计稿生成失败: {e}")
# 6) 模板选择按 SPU.mark 决定: # 6) 模板选择按 SPU.mark 决定:
# mark==1 → 新三图合成模板 MODEL_WEAR_PROMPT(图1模特 + 图2印花设计 + 图3底图) # mark==1 → 新三图合成模板 MODEL_WEAR_PROMPT(图1模特 + 图2印花设计 + 图3底图)
# mark!=1 → 旧两图合成模板 composite_prompt(底图 + 印花设计) # mark!=1 → 旧两图合成模板 composite_prompt(底图 + 印花设计)
# mark==1 统一只做三合一:无模特图时跳过合成,不再回退两图合成(印花+底图) # mark==1 统一只做三合一:无图(模特/平铺)时跳过合成,不再回退两图合成(印花+底图)
if int(spu.get("mark") or 0) == 1: if int(spu.get("mark") or 0) == 1:
print(f"{tag} SPU {spu['code']} mark=1 → 使用三图合成模板(图1模特+图2印花+图3底图)") kind_label = "平铺" if model_kind == "flat" else "模特"
print(f"{tag} SPU {spu['code']} mark=1,图源={kind_label} → 使用三图合成模板(图1{kind_label}+图2印花+图3底图)")
if model_img is not None: if model_img is not None:
# 任务级模特分配(product_node 预分配:一个 SPU 一个模特,SPU 数>模特数循环兜底 # 任务级图源分配(pipeline 预分配:mark=1 在有图的模特/平铺文件夹间随机抽
model_copy = prod_dir / f"{img_code}_model{model_img.suffix}" model_copy = prod_dir / f"{img_code}_{model_kind}{model_img.suffix}"
shutil.copy2(model_img, model_copy) shutil.copy2(model_img, model_copy)
result["model_path"] = str(model_copy) result["model_path"] = str(model_copy)
result["model_folder"] = model_img.parent.name result["model_folder"] = model_img.parent.name
print(f"{tag} 模特图(任务级分配,{model_img.parent.name}/: {model_copy}") result["model_kind"] = model_kind
print(f"{tag} {kind_label}图(任务级分配,{model_img.parent.name}/: {model_copy}")
else: else:
print(f"{tag} material_library 无模特图,mark=1 统一只做三合一,跳过合成") print(f"{tag} material_library 无{kind_label}图,mark=1 统一只做三合一,跳过合成")
else: else:
print(f"{tag} SPU {spu['code']} mark={spu.get('mark')} → 使用两图合成模板 composite_prompt(底图+印花)") print(f"{tag} SPU {spu['code']} mark={spu.get('mark')} → 使用两图合成模板 composite_prompt(底图+印花)")
# 7) 合成: # 7) 合成:
# 有模特图 → 三图合成(图1=模特 / 图2=印花设计 / 图3=底图) # mark=1 有图(模特/平铺)→ 三图合成(图1=模特或平铺 / 图2=印花设计 / 图3=底图)
# mark=1 无模特图 → 跳过合成(统一只做三合一,不做印花+底图两图合成) # mark=1 无图 → 跳过合成(统一只做三合一,不做印花+底图两图合成)
# mark!=1 无模特图 → 两图合成平铺服装图(图3=底图 + 图2=印花设计) # mark!=1 无图 → 两图合成平铺服装图(图3=底图 + 图2=印花设计)
if "design_path" not in result: if "design_path" not in result:
print(f"{tag} 无设计稿,跳过合成") print(f"{tag} 无设计稿,跳过合成")
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:
# 三图合成:优先用简报的 composite_prompt(模板化三图文案),回退内置 MODEL_WEAR_PROMPT # 三图合成:按图源类型选提示词(优先读 prompts/*.md,否则内置 MODEL_WEAR/FLAT_LAY
wear_prompt = (brief.get("composite_prompt") or "").strip() or MODEL_WEAR_PROMPT wear_prompt = _active_prompt(model_kind)
print(f"{tag} 三图合成提交中(3 参考图 img2img,网关处理约 2-6 分钟,请耐心等待)…") kind_label = "平铺" if model_kind == "flat" else "模特"
print(f"{tag} {kind_label}三图合成提交中(3 参考图 img2img,网关处理约 2-6 分钟,请耐心等待)…")
t0 = time.time() t0 = time.time()
ib.print(wear_prompt, str(model_img), composite_path, ib.print(wear_prompt, str(model_img), composite_path,
brief.get("composite_negative", ""), brief.get("composite_negative", ""),
extra_images=[design_path, str(basemap_img)], # 图2印花, 图3底图 extra_images=[design_path, str(basemap_img)], # 图2印花, 图3底图
size=compose_size) # 合成图尺寸按 config compose.size size=compose_size) # 合成图尺寸按 config compose.size
result["composite_path"] = composite_path result["composite_path"] = composite_path
print(f"{tag} 三图模特合成图已生成(耗时 {int(time.time()-t0)}s: {composite_path}") print(f"{tag} {kind_label}三图合成图已生成(耗时 {int(time.time()-t0)}s: {composite_path}")
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
# 合成失败 → 带退避重试(网关超载/超时常见,重试 3 次) # 合成失败 → 带退避重试(网关超载/超时常见,重试 3 次)
print(f"{tag} 三图合成失败,退避重试…: {e}") if _fatal(e):
retried = _retry_image(ib.print, wear_prompt, str(model_img), composite_path, print(f"{tag} 图像服务 503,终止: {e}")
brief.get("composite_negative", ""),
extra_images=[design_path, str(basemap_img)], size=compose_size)
if retried is not None:
result["composite_path"] = composite_path
print(f"{tag} 三图合成重试成功(耗时 {int(time.time()-t0)}s: {composite_path}")
else:
errors.append({"node": "product", "type": type(e).__name__, "message": f"模特合成失败(重试仍失败): {e}", "trace": ""})
print(f"{tag} 三图合成重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}")
return None return None
print(f"{tag} 三图合成失败,退避重试…: {e}")
try:
retried = _retry_image(ib.print, wear_prompt, str(model_img), composite_path,
brief.get("composite_negative", ""),
extra_images=[design_path, str(basemap_img)], size=compose_size)
if retried is not None:
result["composite_path"] = composite_path
print(f"{tag} 三图合成重试成功(耗时 {int(time.time()-t0)}s: {composite_path}")
else:
errors.append({"node": "product", "type": type(e).__name__, "message": f"模特合成失败(重试仍失败): {e}", "trace": ""})
print(f"{tag} 三图合成重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}")
return None
except Exception as e2: # noqa: BLE001
if _fatal(e2):
print(f"{tag} 图像服务 503(重试时),终止: {e2}")
return None
raise
else: else:
# mark=1 无模特图 → 统一只做三合一,不做印花+底图两图合成 # mark=1 无模特图 → 统一只做三合一,不做印花+底图两图合成
if int(spu.get("mark") or 0) == 1: if int(spu.get("mark") or 0) == 1:
@@ -289,17 +380,26 @@ def _process_spu(
result["printed_path"] = printed_path result["printed_path"] = printed_path
print(f"{tag} 平铺服装图已生成(无模特,底图+印花): {printed_path}") print(f"{tag} 平铺服装图已生成(无模特,底图+印花): {printed_path}")
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
print(f"{tag} 平铺服装图失败,退避重试…: {e}") if _fatal(e):
retried = _retry_image(ib.print, flat_prompt, str(basemap_img), printed_path, print(f"{tag} 图像服务 503,终止: {e}")
brief.get("composite_negative", ""),
extra_images=[design_path], size=compose_size)
if retried is not None:
result["printed_path"] = printed_path
print(f"{tag} 平铺服装图重试成功: {printed_path}")
else:
errors.append({"node": "product", "type": type(e).__name__, "message": f"平铺服装图生成失败(重试仍失败): {e}", "trace": ""})
print(f"{tag} 平铺服装图重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}")
return None return None
print(f"{tag} 平铺服装图失败,退避重试…: {e}")
try:
retried = _retry_image(ib.print, flat_prompt, str(basemap_img), printed_path,
brief.get("composite_negative", ""),
extra_images=[design_path], size=compose_size)
if retried is not None:
result["printed_path"] = printed_path
print(f"{tag} 平铺服装图重试成功: {printed_path}")
else:
errors.append({"node": "product", "type": type(e).__name__, "message": f"平铺服装图生成失败(重试仍失败): {e}", "trace": ""})
print(f"{tag} 平铺服装图重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}")
return None
except Exception as e2: # noqa: BLE001
if _fatal(e2):
print(f"{tag} 图像服务 503(重试时),终止: {e2}")
return None
raise
# 7.2) 多色:单 SPU 多色时每个颜色再执行一次三合一(用各自底图),轮播图按颜色路由 # 7.2) 多色:单 SPU 多色时每个颜色再执行一次三合一(用各自底图),轮播图按颜色路由
color_composites: List[Dict[str, Any]] = [] color_composites: List[Dict[str, Any]] = []
@@ -314,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(MODEL_WEAR_PROMPT, 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
@@ -322,6 +422,9 @@ def _process_spu(
color_composites.append({"sku_code": sc, "color": col, "composite_path": cp}) color_composites.append({"sku_code": sc, "color": col, "composite_path": cp})
print(f"{tag} 颜色 {sc}{col})三合一已生成: {cp}") print(f"{tag} 颜色 {sc}{col})三合一已生成: {cp}")
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
if _fatal(e):
print(f"{tag} 颜色 {sc} 三合一遇 503,终止: {e}")
return None
errors.append({"node": "product", "type": type(e).__name__, errors.append({"node": "product", "type": type(e).__name__,
"message": f"颜色 {sc} 三合一失败: {e}", "trace": ""}) "message": f"颜色 {sc} 三合一失败: {e}", "trace": ""})
result["color_composites"] = color_composites result["color_composites"] = color_composites
@@ -445,23 +548,27 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
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)
# 4.1) 任务级模特分配(material_library-<category>): # 4.1) 任务级图源分配(与 Pinterest 模式一致):按 config.product.mark_dirs 可配置映射,
# 一个 SPU 对应一个模特;SPU(不同款)数 > 模特数 → 从全部模特循环兜底(允许重复 # 在「模特图/平铺图」两个有图的文件夹间随机抽图,每张携带 kind(model/flat
model_assign: Dict[str, Any] = {} # → product_node 用对应提示词合成(模特三图 / 平铺三图)
_all_models: List[str] = [] model_assign: Dict[int, Any] = {}
mark_dirs = pcfg.get("mark_dirs") or {}
try: try:
_folder, _all_models = find_first_model_folder(material_root, category) from graph.product import build_mark_sources
sources = build_mark_sources(material_root, mark_dirs, category)
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
_all_models = [] sources = {"model": [], "flat": []}
if _all_models: source_pool = []
seen_spu: Dict[str, str] = {} for kind in ("model", "flat"):
for _i, (_spu, _skus, _tb) in enumerate(worklist): for p in sources.get(kind) or []:
code = _spu.get("code", "") source_pool.append((p, kind))
if code not in seen_spu: if source_pool:
seen_spu[code] = _all_models[_i % len(_all_models)] # SPU>模特数 → 循环兜底 for _i in range(len(worklist)):
model_assign[code] = seen_spu[code] img, kind = random.choice(source_pool) # 每任务随机抽一张(含 kind
print(f"[product] 任务级模特分配:{len(seen_spu)} 个 SPU,模特池 {len(_all_models)}" model_assign[_i] = {"img": img, "kind": kind}
f"{'SPU>模特,循环兜底)' if len(seen_spu) > len(_all_models) else ''}") print(f"[product] 任务级图源分配:{len(model_assign)} 个产品任务(mark_dirs 模特/平铺图随机抽,含 kind)")
else:
print("[product] material_library 无任何可用图(模特/平铺均无)→ 跳过合成,仅导出模板")
# 5) compose 节点生成的共享设计稿(图2):每个任务用自己的热点简报设计(tb.design_path), # 5) compose 节点生成的共享设计稿(图2):每个任务用自己的热点简报设计(tb.design_path),
# 一个货号对应一个设计(多颜色共用该设计),不再所有产品共用第一个 # 一个货号对应一个设计(多颜色共用该设计),不再所有产品共用第一个
@@ -503,8 +610,11 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
concurrency = int(pcfg.get("concurrency") or 0) or min(len(worklist), 5) concurrency = int(pcfg.get("concurrency") or 0) or min(len(worklist), 5)
print(f"[product] 并发 {concurrency}(每 SPU 一线程,上限 {concurrency})处理 {len(worklist)} 个产品任务") print(f"[product] 并发 {concurrency}(每 SPU 一线程,上限 {concurrency})处理 {len(worklist)} 个产品任务")
def _run_one(idx: int, spu, skus, tb): _safe_errors = ThreadSafeErrors()
def _run_one(wi: int, spu, skus, tb):
"""并发执行单个产品:返回 (result or None, img_code)。失败由 _process_spu 内部兜底。""" """并发执行单个产品:返回 (result or None, img_code)。失败由 _process_spu 内部兜底。"""
idx = start_idx + wi # 实际货号序号(start_idx 起自动续号)
img_code = f"{prefix}{idx:03d}" # 货号:图片按此命名(DG000_design.png…) img_code = f"{prefix}{idx:03d}" # 货号:图片按此命名(DG000_design.png…)
try: try:
# 每个任务用自己的热点设计(designs_map),并拷贝为货号命名(designs/DG000_design.png # 每个任务用自己的热点设计(designs_map),并拷贝为货号命名(designs/DG000_design.png
@@ -518,10 +628,12 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
design_path = design_src design_path = design_src
tb = dict(tb) tb = dict(tb)
tb["design_path"] = design_path tb["design_path"] = design_path
_src = model_assign.get(wi) or {}
r = _process_spu(db_path, basemap_root, material_root, category, prod_dir, r = _process_spu(db_path, basemap_root, material_root, category, prod_dir,
tb, ib, spu, skus, pcfg, errors, design_path, title_backend, tb, ib, spu, skus, pcfg, _safe_errors, design_path, title_backend,
country, img_code=img_code, country, img_code=img_code,
model_img=model_assign.get(spu.get("code", "")), model_img=_src.get("img"), # 按任务序号取独立随机图源
model_kind=_src.get("kind", "model"),
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:
@@ -535,7 +647,7 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
# 货号自动续号:任务一开始全部按序分配(start_idx 起),不覆盖已生成的产物 # 货号自动续号:任务一开始全部按序分配(start_idx 起),不覆盖已生成的产物
start_idx = _next_img_idx(prod_dir, prefix) start_idx = _next_img_idx(prod_dir, prefix)
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex: with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex:
futures = [ex.submit(_run_one, start_idx + i, spu, skus, tb) futures = [ex.submit(_run_one, i, spu, skus, tb)
for i, (spu, skus, tb) in enumerate(worklist)] for i, (spu, skus, tb) in enumerate(worklist)]
for f in concurrent.futures.as_completed(futures): for f in concurrent.futures.as_completed(futures):
r, img_code = f.result() r, img_code = f.result()
@@ -545,6 +657,9 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
_record_used(cache_dir, r) # (热点-风格) 去重记录 → 缓存根目录 _record_used(cache_dir, r) # (热点-风格) 去重记录 → 缓存根目录
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
pass pass
# 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加)
if len(_safe_errors):
errors.extend(list(_safe_errors))
results.sort(key=lambda x: x.get("img_code", "")) # 按货号排序,模板/清单顺序稳定 results.sort(key=lambda x: x.get("img_code", "")) # 按货号排序,模板/清单顺序稳定
_write_products(prod_dir, results) _write_products(prod_dir, results)
+75 -18
View File
@@ -22,6 +22,43 @@ PINTEREST_PRINT_SUFFIX = (
"no garment, no shirt, no model, no mannequin, no watermark" "no garment, no shirt, no model, no mannequin, no watermark"
) )
# Pinterest 生图提示词 3 段结构中第 2 段(仅当设计含文字时追加):
# 要求模型把引号内的文字按原文逐字正确拼写,避免乱码/拼错
SPELLING_RULE = (
"Render every phrase shown in quotes exactly as written, "
"correctly spelled."
)
# review(疑似商标/受保护主题)简报统一追加的「原创化魔改」引导段:
# 只做风格参考,禁复刻品牌/商标/角色,换名换细节,生成通用非侵权致敬式设计。
# 两个模式(热点采集 / Pinterest 参考)共用同一文本,避免不一致。
REVIEW_REBRAND_HINT = (
"IMPORTANT: this theme is ONLY a loose stylistic reference. "
"Do NOT reproduce any brand logo, trademark, character, mascot, copyrighted artwork or real person. "
"Create a fully ORIGINAL design with a different name and distinct visual details and colors — "
"a generic, non-infringing homage in the same mood, clearly distinct from the original."
)
# —— 自定义模式(custom)生图模板:固定前缀 + 分析模型 image_prompt + 固定负向 ——
# 自定义模式分析模型产出的是「新设计描述」,生图时套用这套固定模板(含防复制/防服装约束),
# 负向用固定文本(写入 composite_negativecompose 生图时作为负向参数传给图像后端)。
CUSTOM_IMAGE_PROMPT_TEMPLATE = (
"Use the attached bestseller product photo only as loose inspiration for "
"overall mood, theme, era and style genre — do NOT reproduce, trace, "
"rearrange, recolor or closely imitate its printed artwork, characters, "
"layout or text, and do NOT render a shirt, garment, model, hanger, photo "
"scene, product mockup or background of any kind. First, carefully examine "
"the printed artwork on the garment in the attached photo: zoom in mentally "
"on the print area, identify its subject, technique, palette and layout, and "
"base the new design on THOSE observed traits. Ignore the model, background "
"and photo quality. Then: {image_prompt}"
)
CUSTOM_NEGATIVE_PROMPT = (
"copy of reference artwork, lookalike of the bestseller print, characters, "
"mascots, likenesses, logos, trademarks, watermark, photorealistic shirt, "
"apparel, product mockup, model, garment, hanger, busy background"
)
# 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif, # 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif,
# 避免 gpt-image 等内容策略频繁拦截导致"生图限制多") # 避免 gpt-image 等内容策略频繁拦截导致"生图限制多")
_IMG_RISKY_SWAP = { _IMG_RISKY_SWAP = {
@@ -55,6 +92,17 @@ def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]:
cc = state["country_config"] cc = state["country_config"]
extra_rules = cc.get("extra_style_rules") or [] extra_rules = cc.get("extra_style_rules") or []
tpls = config.get("prompt_templates") or {} tpls = config.get("prompt_templates") or {}
# Pinterest 生图提示词固定段:可配置(config.pinterest.prompt_pieces),留空/缺失回退内置常量。
# 自定义模式(custom_mode)用独立的一套固定段(config.custom.prompt_pieces),默认与 Pinterest 相同、可单独编辑。
if bool(state.get("custom_mode")):
pp = (config.get("custom") or {}).get("prompt_pieces") or {}
else:
pp = (config.get("pinterest") or {}).get("prompt_pieces") or {}
print_suffix = (pp.get("print_suffix") or "").strip() or PINTEREST_PRINT_SUFFIX
spelling_rule = (pp.get("spelling_rule") or "").strip() or SPELLING_RULE
review_rebrand_hint = (pp.get("review_rebrand_hint") or "").strip() or REVIEW_REBRAND_HINT
custom_ip_tpl = (pp.get("image_prompt_template") or "").strip() or CUSTOM_IMAGE_PROMPT_TEMPLATE
custom_neg = (pp.get("negative_prompt") or "").strip() or CUSTOM_NEGATIVE_PROMPT
briefs: List[Dict[str, Any]] = [] briefs: List[Dict[str, Any]] = []
for r in screened: for r in screened:
@@ -73,27 +121,36 @@ def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]:
composition = (r.get("composition") or derive_composition(r["topic"], r.get("design_category"))).strip() composition = (r.get("composition") or derive_composition(r["topic"], r.get("design_category"))).strip()
prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country) prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country)
# Pinterest 简报:直接用 LLM 多模态对图片的描述拼接的 image_prompt(跳过四要素模板),
# 仅追加统一的「小印花 + 纯白底」约束段;wearable/composite 仍用模板装配。
llm_ip = (r.get("image_prompt") or "").strip() llm_ip = (r.get("image_prompt") or "").strip()
if r.get("source") == "pinterest" and llm_ip: if r.get("source") == "pinterest" and llm_ip:
prompts["image_prompt"] = llm_ip + PINTEREST_PRINT_SUFFIX if bool(state.get("custom_mode")):
print(f"[prompt] Pinterest 简报用 LLM 多模态描述作为 image_prompt(跳过四要素模板): 「{r['topic']}") # —— 自定义模式:固定模板(前缀 + 分析 image_prompt),负向用固定文本 ——
# 文字印花(约 30% 概率):简报有 slogan 时,随机注入文字段到设计稿提示词 # 模板含防复制/防服装约束,不再追加 print_suffix;负向写入 composite_negative
slogan = (r.get("slogan") or "").strip() # compose 生图时作为负向参数传给图像后端。
if slogan and random.random() < float(config.get("prompt_templates", {}).get("text_ratio", 0.3)): prompts["image_prompt"] = custom_ip_tpl.replace("{image_prompt}", llm_ip)
text_seg = (f', with the text "{slogan}" rendered as bold retro typography, ' prompts["composite_negative"] = custom_neg
f'lettering clean and correctly spelled, high contrast, as the focal text of the print') print(f"[prompt] 自定义模式按固定模板拼 image_prompt(含固定负向): 「{r['topic']}")
prompts["image_prompt"] = prompts["image_prompt"] + text_seg else:
r["used_slogan"] = slogan # —— Pinterest 参考模式:跳过四要素模板,按 3 段结构拼 image_prompt ——
# review(疑似商标/受保护主题)→ 动态注入「原创化魔改」引导:只做风格参考,禁止复刻品牌/商标/角色, # ① image_prompt(分析模型产出) + 固定输出形态后缀 PINTEREST_PRINT_SUFFIX
# 换名换细节,生成通用非侵权的致敬式设计 # ② 拼写锁定句 SPELLING_RULE(仅当 LLM image_prompt 已含引号文字段时)
# 是否含文字、拼写与否均由分析模型产出决定,本模式不注入 slogan。
seg: List[str] = [llm_ip, print_suffix]
if '"' in llm_ip:
seg.append(spelling_rule)
prompts["image_prompt"] = ", ".join(seg)
print(f"[prompt] Pinterest 简报按 3 段结构拼 image_prompt(跳过四要素模板): 「{r['topic']}")
else:
# —— 热点采集模式:四要素模板装配 + 文字印花(约 30% 概率注入 slogan)——
slogan = (r.get("slogan") or "").strip()
if slogan and random.random() < float(config.get("prompt_templates", {}).get("text_ratio", 0.3)):
text_seg = (f', with the text "{slogan}" rendered as bold retro typography, '
f'lettering clean and correctly spelled, high contrast, as the focal text of the print')
prompts["image_prompt"] = prompts["image_prompt"] + text_seg
r["used_slogan"] = slogan
# review(疑似商标/受保护主题)→ 追加「原创化魔改」引导(两个模式通用)
if str(r.get("risk_level", "")).strip().lower() == "review": if str(r.get("risk_level", "")).strip().lower() == "review":
prompts["image_prompt"] = (prompts["image_prompt"] prompts["image_prompt"] = prompts["image_prompt"] + " " + review_rebrand_hint
+ " IMPORTANT: this theme is ONLY a loose stylistic reference. "
"Do NOT reproduce any brand logo, trademark, character, mascot, copyrighted artwork or real person. "
"Create a fully ORIGINAL design with a different name and distinct visual details and colors — "
"a generic, non-infringing homage in the same mood, clearly distinct from the original.")
print(f"[prompt] review 简报注入原创化魔改引导: 「{r['topic']}") print(f"[prompt] review 简报注入原创化魔改引导: 「{r['topic']}")
r.update(prompts) r.update(prompts)
r["motif"] = motif r["motif"] = motif
+4 -2
View File
@@ -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
+56 -10
View File
@@ -20,14 +20,8 @@ from graph.validate import with_fallback
def _template_out_path(prod_dir: Path, tpl_name: str) -> Path: def _template_out_path(prod_dir: Path, tpl_name: str) -> Path:
"""模板输出路径:默认 {tpl_name}_已填写.xlsx;已存在/被占用则自动换名加序号(同款号多产品不互相覆盖)。""" """模板输出路径:默认 {tpl_name}_已填写.xlsx;已存在/被占用则自动换名加序号(同款号多产品不互相覆盖)。"""
base = prod_dir / f"{tpl_name}_已填写.xlsx" base = prod_dir / f"{tpl_name}_已填写.xlsx"
try: if not base.exists():
with open(base, "ab"): return base
pass
except OSError:
pass
else:
if not base.exists():
return base
for i in range(2, 100): for i in range(2, 100):
cand = prod_dir / f"{tpl_name}_已填写_{i}.xlsx" cand = prod_dir / f"{tpl_name}_已填写_{i}.xlsx"
if not cand.exists(): if not cand.exists():
@@ -35,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 []
@@ -73,7 +99,9 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
db_path = root / db_path db_path = root / db_path
break break
from graph.template_export import export_products from graph.template_export import (export_products, _resolve_component_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)
@@ -95,6 +123,7 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
sku_codes = [r.get("sku_code") or ""] sku_codes = [r.get("sku_code") or ""]
batch.append({ batch.append({
"spu_code": r.get("spu_code", ""), "spu_code": r.get("spu_code", ""),
"img_code": r.get("img_code", ""),
"sku_codes": sku_codes, "sku_codes": sku_codes,
"images": [], "images": [],
"spu_per_color": True, # 每颜色一个独立 SPU 块(单色多 SPU) "spu_per_color": True, # 每颜色一个独立 SPU 块(单色多 SPU)
@@ -107,19 +136,36 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
"seed_shot_urls": r.get("seed_shot_urls") or [], "seed_shot_urls": r.get("seed_shot_urls") or [],
}) })
# 写入模板前按货号(前端自定义前缀+3位计数,如 DG001)最后3位从小到大排序,按顺序插入
def _tail_num(rec: Dict[str, Any]) -> tuple:
code = str(rec.get("img_code") or rec.get("oss_code") or "")
try:
return (0, int(code[-3:]))
except ValueError:
return (1, 0)
batch.sort(key=_tail_num)
exported: List[str] = [] exported: List[str] = []
if batch: if batch:
out = _template_out_path(prod_dir, "商品上传") # 输出文件名 = 模板原文件名 + _已填写(如 NEW-波兰男黑T恤_已填写.xlsx
out = _template_out_path(prod_dir, Path(tp).stem)
try: try:
out = export_products( out = export_products(
db_path, batch, tdir, tp, str(out), db_path, batch, tdir, tp, str(out),
markup_percent=float(pcfg.get("markup_percent") or 0), markup_percent=float(pcfg.get("markup_percent") or 0),
suggested_price_ratio=float(pcfg.get("suggested_price_ratio") or 0),
component_map=_resolve_component_map(config),
season_map=_resolve_season_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": ""})
-7
View File
@@ -88,10 +88,3 @@ def build_oss_key(country: str, timestamp: str, code: str, rand4: str, ext: str
"""对象 key{国家}/{时间戳}/{货号}_{4位随机}.jpg(如 GB/20260820150945/DG000_Ab3x.jpg)。""" """对象 key{国家}/{时间戳}/{货号}_{4位随机}.jpg(如 GB/20260820150945/DG000_Ab3x.jpg)。"""
safe = lambda s: "".join(c for c in (s or "") if c.isalnum() or c in "-_").strip() safe = lambda s: "".join(c for c in (s or "") if c.isalnum() or c in "-_").strip()
return f"{safe(country)}/{safe(timestamp)}/{safe(code)}_{safe(rand4)}{ext}" return f"{safe(country)}/{safe(timestamp)}/{safe(code)}_{safe(rand4)}{ext}"
def random_code4() -> str:
"""4 位随机:大小写英文 + 数字。"""
import random
import string
return "".join(random.choices(string.ascii_letters + string.digits, k=4))
+36
View File
@@ -179,6 +179,42 @@ def design_md5_ok(image_path: str) -> bool:
_IMG_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp") _IMG_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp")
def list_valid_images(folder: Any) -> List[Path]:
"""返回文件夹内所有有效图片文件(扩展名匹配,含子目录递归),用于自定义模式。
供图源目录/自定义模式统计「有效图片数量」与注册图池使用;空/不存在返回空列表。
"""
d = Path(folder)
if not d.is_dir():
return []
out: List[Path] = []
for f in sorted(d.rglob("*")):
if f.is_file() and f.suffix.lower() in _IMG_EXTS:
out.append(f)
return out
def validate_custom_images(folder: Any, target: int) -> tuple:
"""自定义模式数量校验:返回 (ok, valid_n, message)。
folder 未填/不存在/无有效图片 → 失败;有效图片数 < 选品清单总数 target → 失败
(「选品清单不得大于有效图片数」硬校验)。供 UI/入口 fail-fast 与节点安全网复用。
"""
folder_s = str(folder or "").strip()
if not folder_s:
return False, 0, "自定义图片文件夹未填写(pinterest.custom_image_dir / UI 选择上传文件夹)"
if not Path(folder_s).is_dir():
return False, 0, f"自定义图片文件夹不存在:{folder_s}"
imgs = list_valid_images(folder_s)
n = len(imgs)
if n == 0:
return False, 0, f"自定义图片文件夹「{folder_s}」中没有有效图片(jpg/jpeg/png/webp"
if target > n:
return False, n, (f"选品清单数量({target})大于自定义图片有效数量({n}):"
f"选品清单不得大于有效图片数,请补充图片或减少选品")
return True, n, f"自定义图片校验通过:有效图片 {n} 张 ≥ 选品清单 {target}"
def image_pool_path(output_dir: str, country: str) -> Path: def image_pool_path(output_dir: str, country: str) -> Path:
return Path(output_dir) / "pinterest_ref" / country / "image_pool.json" return Path(output_dir) / "pinterest_ref" / country / "image_pool.json"
+241 -26
View File
@@ -12,6 +12,7 @@
并发上限与 product_node 一致(默认 5),避免压垮图像网关。 并发上限与 product_node 一致(默认 5),避免压垮图像网关。
""" """
import concurrent.futures import concurrent.futures
import random
import re import re
import threading import threading
import time import time
@@ -35,13 +36,23 @@ class PinterestPipeline:
self._briefs: List[Dict[str, Any]] = [] self._briefs: List[Dict[str, Any]] = []
self._done = False self._done = False
self._cursor = 0 self._cursor = 0
# 正在处理(已提交线程池、尚未完成)的简报数,供路由判断简报池是否空闲
self._in_flight = 0
# 结果 # 结果:每完成一个产品立即落盘追加写入 products_pending.jsonl
# 内存列表仅作缓存(finish 时再读盘合并),中途崩溃也不丢已完成产品。
self._products: List[Dict[str, Any]] = [] self._products: List[Dict[str, Any]] = []
self._products_lock = threading.Lock() self._products_lock = threading.Lock()
self._errors: List[Dict[str, Any]] = [] self._errors: List[Dict[str, Any]] = []
self._errors_lock = threading.Lock() self._errors_lock = threading.Lock()
# 致命图像服务错误(53/账户不可用):置位后终止分发、丢弃未完成简报,仅保留已完成产品
self._fatal_lock = threading.Lock()
self._fatal_503 = False
# 已完成产品落盘文件(JSONL 追加写):output/<country>/<ts>/products_pending.jsonl
self._pending_file = self.output_dir / "products_pending.jsonl"
# 路径解析(复用 product_node 的 _abs 逻辑:运行根优先,其次数据根) # 路径解析(复用 product_node 的 _abs 逻辑:运行根优先,其次数据根)
pcfg = self.config.get("product") or {} pcfg = self.config.get("product") or {}
@@ -94,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]}…」无男/女 → 男女模特随机")
@@ -188,20 +200,49 @@ class PinterestPipeline:
return None return None
def _assign_models(self) -> Dict[str, Any]: def _assign_models(self) -> Dict[str, Any]:
model_assign: Dict[str, Any] = {} """任务级图源分配:按每个任务 spu.mark 从可配置的「模特图/平铺图」文件夹间随机抽图。
每个任务独立随机抽一张:先合并该 mark 对应「有图的」模特/平铺目录的全部合格图,再从其中随机抽一张,
抽到哪个文件夹的图就返回对应 kindmodel/flat),供 product_node 用对应提示词合成。
某类目录无图则只用另一类;两类都无图则该任务无图源(跳过合成)。
返回 {task_key: {"img": Path, "kind": "model"|"flat", "prompts": {…}}}。
"""
import random as _random
assign: Dict[str, Any] = {}
pcfg = self.config.get("product") or {}
mark_dirs = pcfg.get("mark_dirs") or {}
try: try:
from graph.product import find_first_model_folder from graph.product import build_mark_sources
_folder, _all_models = find_first_model_folder(self._material_root, self._category) except Exception as e: # noqa: BLE001
except Exception: # noqa: BLE001 print(f"[pinterest_pipeline] 图源映射导入失败: {e}")
_all_models = [] return assign
if _all_models: # 每个出现过的 mark 各建一个图源池,避免多 mark 错配
seen: Dict[str, str] = {} pool_by_mark: Dict[str, list] = {}
for _i, (spu, _skus) in enumerate(self._worklist): for _i, (spu, _skus) in enumerate(self._worklist):
code = spu.get("code", "") mark = str(spu.get("mark") or "").strip() or "1"
if code not in seen: if mark in pool_by_mark:
seen[code] = _all_models[_i % len(_all_models)] continue
model_assign[code] = seen[code] try:
return model_assign sources = build_mark_sources(self._material_root, mark_dirs, self._category, mark=mark)
except Exception as e: # noqa: BLE001
print(f"[pinterest_pipeline] mark={mark} 图源构建失败: {e}")
sources = {"model": [], "flat": []}
pool = []
for kind in ("model", "flat"):
for p in sources.get(kind) or []:
pool.append((p, kind))
pool_by_mark[mark] = pool
if pool:
print(f"[pinterest_pipeline] mark={mark} 图源池:{len(pool)} 张(模特/平铺)")
for _i, (spu, _skus) in enumerate(self._worklist):
key = f"task_{_i}"
mark = str(spu.get("mark") or "").strip() or "1"
pool = pool_by_mark.get(mark) or []
if not pool:
continue
img, kind = _random.choice(pool) # 每任务独立随机抽一张(含 kind)
assign[key] = {"img": img, "kind": kind}
return assign
def _load_materials(self) -> Dict[str, str]: def _load_materials(self) -> Dict[str, str]:
material_map: Dict[str, str] = {} material_map: Dict[str, str] = {}
@@ -237,6 +278,48 @@ class PinterestPipeline:
with self._err400_lock: with self._err400_lock:
return self._err400_aborted return self._err400_aborted
# ------------------------------------------------------------------ #
# 致命图像服务错误(503 / 账户不可用):重试无效,提前终止整个任务
# ------------------------------------------------------------------ #
@staticmethod
def is_fatal_503(exc) -> bool:
"""判断异常是否为「图像服务不可用」类致命错误(503 / No available compatible accounts)。
这类错误说明账户配额耗尽或网关故障,重试必然失败,应提前终止任务而非无意义重试。
"""
msg = str(exc)
if "503" in msg:
return True
low = msg.lower()
return "no available compatible accounts" in low or "account" in low and "not available" in low
def record_503(self) -> bool:
"""记录一次致命 503:首次触发即置位终止标志(后续请求直接短路不再提交)。
返回 True 表示本次触发终止(调用方应立即停止当前链路)。
"""
with self._fatal_lock:
first = not self._fatal_503
self._fatal_503 = True
if first:
print("[pinterest_pipeline] ⛔ 检测到图像服务 503No available compatible accounts),"
"重试无效 → 提前终止任务,未完成产品将废弃,仅保留已完成产品")
return first
def is_fatal_503_aborted(self) -> bool:
with self._fatal_lock:
return self._fatal_503
def abort_unfinished(self) -> None:
"""终止分发:丢弃简报池中所有未完成简报(已完成的落盘产品保留)。"""
with self._cond:
dropped = len(self._briefs)
self._briefs = []
self._done = True
self._cond.notify_all()
if dropped:
print(f"[pinterest_pipeline] 503 终止:丢弃未完成简报 {dropped} 条(未完成产品废弃)")
def _abort_current_term(self) -> None: def _abort_current_term(self) -> None:
"""放弃当前种子词:清空其未完成简报 + 图池未消费图片(已完成的保留)。""" """放弃当前种子词:清空其未完成简报 + 图池未消费图片(已完成的保留)。"""
term = self._err400_term term = self._err400_term
@@ -298,18 +381,63 @@ class PinterestPipeline:
self._cond.notify_all() self._cond.notify_all()
print(f"[pinterest_pipeline] 简报池 +{len(briefs)} 条(待处理 {len(self._briefs)}") print(f"[pinterest_pipeline] 简报池 +{len(briefs)} 条(待处理 {len(self._briefs)}")
def pending_count(self) -> int:
"""简报池中待处理 + 正在处理的简报数(供路由判断是否需要补图/补分析)。"""
with self._cond:
queued = len(self._briefs)
return queued + self._in_flight
def wait_idle(self, timeout: Optional[float] = None) -> bool:
"""阻塞等待简报池消化完(无待处理且无在途),返回是否已空闲。
用 Condition 等待(_process_one 完成时 notify_all 唤醒),而非轮询 sleep
避免 wait 循环疯狂刷屏。timeout 为 None 时无限等待(受 _done 保护)。
"""
with self._cond:
while (self._briefs or self._in_flight > 0) and not self._done:
if timeout is not None:
deadline = time.time() + timeout
remaining = deadline - time.time()
if remaining <= 0:
return False
self._cond.wait(min(remaining, 1.0))
else:
self._cond.wait()
return not self._briefs and self._in_flight <= 0
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
# 异常路径也确保排空并释放线程池,避免 dispatcher/worker 泄漏
try:
self.finish()
except Exception: # noqa: BLE001
pass
return False
def finish(self) -> tuple: def finish(self) -> tuple:
"""排空简报池、等待全部产品完成,返回 (products, errors)。""" """排空简报池、等待全部产品完成,返回 (products, errors)。
产品来源:手动已完成(内存缓存)+ 落盘文件(products_pending.jsonl
按货号去重合并——即使中途 503 终止/崩溃,已完成产品也不丢。
"""
with self._cond: with self._cond:
self._done = True self._done = True
self._cond.notify_all() self._cond.notify_all()
self._dispatcher.join() self._dispatcher.join()
self._pool.shutdown(wait=True) self._pool.shutdown(wait=True)
# 读盘 + 内存合并去重(内存为准,但以落盘为最终权威——崩溃恢复后走落盘)
pending = self.load_pending()
merged = {str(p.get("img_code", "")): p for p in pending}
with self._products_lock: with self._products_lock:
products = list(self._products) for p in self._products:
merged[str(p.get("img_code", ""))] = p
products = [merged[k] for k in merged if k]
with self._errors_lock: with self._errors_lock:
errors = list(self._errors) errors = list(self._errors)
print(f"[pinterest_pipeline] 收尾:完成 {len(products)} 个产品,错误 {len(errors)}") print(f"[pinterest_pipeline] 收尾:完成 {len(products)} 个产品,错误 {len(errors)}"
f"{'(含落盘恢复 ' + str(len(pending)) + '' if pending else ''}")
return products, errors return products, errors
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@@ -322,8 +450,16 @@ class PinterestPipeline:
self._cond.wait() self._cond.wait()
if self._done and not self._briefs: if self._done and not self._briefs:
break break
if self._fatal_503:
# 致命 503:不再分发新简报(未完成的废弃,仅保留已完成落盘产品)
self._briefs = []
self._done = True
self._cond.notify_all()
break
batch = self._briefs batch = self._briefs
self._briefs = [] self._briefs = []
# 与 _briefs 清空同一临界区递增在途数,避免 wait_idle 误判空闲
self._in_flight += len(batch)
for b in batch: for b in batch:
with self._cond: with self._cond:
idx = self._cursor idx = self._cursor
@@ -335,6 +471,8 @@ class PinterestPipeline:
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def _process_one(self, brief: Dict[str, Any], idx: int) -> None: def _process_one(self, brief: Dict[str, Any], idx: int) -> None:
try: try:
if self.is_fatal_503_aborted():
return
# 0) 先定货号:整条链路(设计/三合一/种草图)都用它命名与匹配,避免序号错位 # 0) 先定货号:整条链路(设计/三合一/种草图)都用它命名与匹配,避免序号错位
if idx >= len(self._worklist): if idx >= len(self._worklist):
print(f"[pinterest_pipeline] 简报 {idx} 无对应产品任务,跳过") print(f"[pinterest_pipeline] 简报 {idx} 无对应产品任务,跳过")
@@ -343,6 +481,8 @@ class PinterestPipeline:
img_code = f"{self._prefix}{idx:03d}" img_code = f"{self._prefix}{idx:03d}"
# 1) 生成设计(compose)——直接按货号命名 designs/{img_code}_design.png # 1) 生成设计(compose)——直接按货号命名 designs/{img_code}_design.png
design_path = self._gen_design(brief, img_code) design_path = self._gen_design(brief, img_code)
if self.is_fatal_503_aborted():
return
if self.is_400_aborted(): if self.is_400_aborted():
# 当前种子词 400 超限已放弃:正在生成的当个也放弃,不进入后续链路 # 当前种子词 400 超限已放弃:正在生成的当个也放弃,不进入后续链路
print(f"[pinterest_pipeline] 当前种子词 400 超限已放弃,跳过简报 {idx}") print(f"[pinterest_pipeline] 当前种子词 400 超限已放弃,跳过简报 {idx}")
@@ -357,13 +497,17 @@ class PinterestPipeline:
return return
brief = new_brief brief = new_brief
design_path = self._gen_design(brief, img_code) design_path = self._gen_design(brief, img_code)
if self.is_fatal_503_aborted():
return
if design_path: if design_path:
break break
if not design_path: if not design_path:
return return
brief["design_path"] = design_path brief["design_path"] = design_path
# 2) 三合一(product)——同一货号 # 2) 三合一(product)——同一货号;task_idx=本次简报序号,模特按任务独立随机
prod = self._process_spu(brief, spu, skus, img_code, design_path) prod = self._process_spu(brief, spu, skus, img_code, design_path, task_idx=idx)
if self.is_fatal_503_aborted():
return
if not prod: if not prod:
return return
# 3) OSS 上传 # 3) OSS 上传
@@ -376,6 +520,8 @@ class PinterestPipeline:
_record_used(self.cache_dir, prod) _record_used(self.cache_dir, prod)
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
pass pass
# 5) 落盘:每完成一个产品立即追加写入 products_pending.jsonl(不依赖内存,崩溃不丢)
self._persist_product(prod)
with self._products_lock: with self._products_lock:
self._products.append(prod) self._products.append(prod)
print(f"[pinterest_pipeline] 产品完成: {prod.get('img_code', '')}" print(f"[pinterest_pipeline] 产品完成: {prod.get('img_code', '')}"
@@ -385,6 +531,45 @@ class PinterestPipeline:
self._errors.append({"node": "pinterest_pipeline", "type": type(e).__name__, self._errors.append({"node": "pinterest_pipeline", "type": type(e).__name__,
"message": f"简报 {idx} 处理失败: {e}", "trace": ""}) "message": f"简报 {idx} 处理失败: {e}", "trace": ""})
print(f"[pinterest_pipeline] 简报 {idx} 处理失败: {e}") print(f"[pinterest_pipeline] 简报 {idx} 处理失败: {e}")
finally:
with self._cond:
self._in_flight = max(0, self._in_flight - 1)
self._cond.notify_all()
def _persist_product(self, prod: Dict[str, Any]) -> None:
"""把已完成产品追加写入 products_pending.jsonlJSONL 每行一个产品)。
并发安全:写入持 _products_lock,整行一次写(含换行),避免并发 append 交织;
落盘失败不阻塞主流程(仅告警);finish() 时读盘合并,保证已完成产品不丢。
"""
try:
import json as _json
self._pending_file.parent.mkdir(parents=True, exist_ok=True)
line = _json.dumps(prod, ensure_ascii=False) + "\n"
with self._products_lock:
with open(self._pending_file, "a", encoding="utf-8") as f:
f.write(line)
except Exception as e: # noqa: BLE001
print(f"[pinterest_pipeline] 产品落盘失败(不影响流程): {e}")
def load_pending(self) -> List[Dict[str, Any]]:
"""读回 products_pending.jsonl 中已落盘的产品(进程重启/崩溃恢复用)。"""
import json as _json
out: List[Dict[str, Any]] = []
if not self._pending_file.exists():
return out
try:
for line in self._pending_file.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
out.append(_json.loads(line))
except Exception: # noqa: BLE001
continue
except Exception as e: # noqa: BLE001
print(f"[pinterest_pipeline] 读回落盘产品失败: {e}")
return out
def _gen_design(self, brief: Dict[str, Any], img_code: str) -> Optional[str]: def _gen_design(self, brief: Dict[str, Any], img_code: str) -> Optional[str]:
if self._ib is None: if self._ib is None:
@@ -398,10 +583,18 @@ class PinterestPipeline:
if self.record_400(): if self.record_400():
self._abort_current_term() self._abort_current_term()
def _on_503():
self.record_503()
self.abort_unfinished()
return generate_design(self._ib, brief, design_dir, img_code, self._errors, return generate_design(self._ib, brief, design_dir, img_code, self._errors,
on_400=_on_400, on_400=_on_400, on_503=_on_503,
size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024")) size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024"))
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
if self.is_fatal_503(e):
self.record_503()
self.abort_unfinished()
return None
with self._errors_lock: with self._errors_lock:
self._errors.append({"node": "compose", "type": type(e).__name__, self._errors.append({"node": "compose", "type": type(e).__name__,
"message": f"设计稿生成失败 {brief.get('topic', '')}: {e}", "message": f"设计稿生成失败 {brief.get('topic', '')}: {e}",
@@ -472,8 +665,19 @@ class PinterestPipeline:
print(f"[pinterest_pipeline] 补充简报装配失败: {e}") print(f"[pinterest_pipeline] 补充简报装配失败: {e}")
return None return None
def _model_source(self, task_idx: int, spu=None) -> Dict[str, Any]:
"""返回某任务给 _process_spu 的图源参数:model_img / model_kind / prompts。"""
src = self._model_assign.get(f"task_{task_idx}") or {}
img = src.get("img")
if img is None:
return {}
if spu is not None and int(spu.get("mark") or 0) != 1:
return {"model_img": img} # 非 mark=1 走旧逻辑(不传 kindproduct_node 按其 mark 自行判定)
return {"model_img": img,
"model_kind": src.get("kind", "model")}
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) -> Optional[Dict[str, Any]]: design_path: str, task_idx: int = 0) -> Optional[Dict[str, Any]]:
from graph.nodes.product_node import _process_spu as _ps from graph.nodes.product_node import _process_spu as _ps
prod_dir = self.output_dir / "product" prod_dir = self.output_dir / "product"
prod_dir.mkdir(parents=True, exist_ok=True) prod_dir.mkdir(parents=True, exist_ok=True)
@@ -483,7 +687,9 @@ class PinterestPipeline:
r = _ps(self._db_path, self._basemap_root, self._material_root, self._category, r = _ps(self._db_path, self._basemap_root, self._material_root, self._category,
prod_dir, brief, self._ib, spu, skus, self.config.get("product") or {}, prod_dir, brief, self._ib, spu, skus, self.config.get("product") or {},
self._errors, design_path, self._title_backend, self.country, self._errors, design_path, self._title_backend, self.country,
img_code=img_code, model_img=self._model_assign.get(spu.get("code", ""))) img_code=img_code,
on_503=lambda: (self.record_503(), self.abort_unfinished()),
**self._model_source(task_idx, spu))
if r: if r:
r["img_code"] = img_code r["img_code"] = img_code
return r return r
@@ -559,9 +765,18 @@ class PinterestPipeline:
if not base or not Path(base).exists(): if not base or not Path(base).exists():
print(f"[pinterest_pipeline] {r.get('spu_code', '')} 参考图缺失,跳过该色种草图") print(f"[pinterest_pipeline] {r.get('spu_code', '')} 参考图缺失,跳过该色种草图")
continue continue
generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir), try:
r.get("composite_negative", ""), generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir),
size=size, prefix=pfx, gender=self._gender) r.get("composite_negative", ""),
size=size, prefix=pfx, gender=self._gender,
retries=int(ss_cfg.get("retries", 3)))
except Exception as e: # noqa: BLE001
if self.is_fatal_503(e):
self.record_503()
self.abort_unfinished()
return
print(f"[pinterest_pipeline] 种草图生成失败(跳过该色): {e}")
continue
paths.extend(generated) paths.extend(generated)
if not paths: if not paths:
return return
+74
View File
@@ -11,6 +11,8 @@ import sqlite3
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from PIL import Image
# 支持的图片格式:模特图/底图均按此识别(png/jpg 等常见格式全覆盖;AVIF/GIF/TIFF 亦支持) # 支持的图片格式:模特图/底图均按此识别(png/jpg 等常见格式全覆盖;AVIF/GIF/TIFF 亦支持)
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".avif", ".gif", ".tiff", ".tif"} IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".avif", ".gif", ".tiff", ".tif"}
@@ -114,6 +116,78 @@ def find_first_model_folder(material_root, preferred: Optional[str] = None):
return None, [] return None, []
def image_ratio_ok(path, target_ratio: float = 3 / 4, tolerance: float = 0.06) -> bool:
"""图片宽高比是否接近目标比例(默认 3:4,相对容差 6%)。
相对容差:接受 [target*(1-tol), target*(1+tol)],对 3:4 即 0.705~0.795。
无法解析的图片(损坏/非标准)按不通过处理,避免坏图被当模特。
"""
try:
with Image.open(path) as im:
w, h = im.size
if w <= 0 or h <= 0:
return False
ratio = w / h
lo = target_ratio * (1 - tolerance)
hi = target_ratio * (1 + tolerance)
return lo <= ratio <= hi
except Exception: # noqa: BLE001
return False
def _dir_images(d: Path, ratio: float, tolerance: float) -> List[Path]:
"""返回目录内满足比例过滤(默认3:4)的图片列表;目录不存在/无图片返回空。"""
if d is None or not d.is_dir():
return []
imgs = [f for f in sorted(d.iterdir()) if f.is_file() and f.suffix.lower() in IMG_EXTS]
if not imgs:
return []
ok = [f for f in imgs if image_ratio_ok(f, ratio, tolerance)]
if len(ok) < len(imgs):
print(f"[product] 目录 {d.name}/ 过滤非 {int(ratio * 100)}:{int(ratio * 100) + 1} 比例:"
f"{len(imgs)}{len(ok)}")
return ok
def build_mark_sources(material_root, mark_dirs: Optional[Dict] = None,
category: str = "T-shirt", mark: str = "1",
ratio: float = 3 / 4, tolerance: float = 0.06) -> Dict[str, Any]:
"""按可配置的 mark→图源映射,返回指定 mark 的图源选择信息。
图源 = {"model": [Path...], "flat": [Path...]}(模特图 / 平铺图,各带英文键)。
从 config.product.mark_dirs 读 mark 对应的两个子目录名(model_dir/flat_dir),
在 material_library 下解析 → 过滤 3:4 → 返回「有图的文件夹」图片列表。
mark 未配置时回退 mark_dirs["1"];完全无配置则回退 category 默认目录。
返回结构:{"model": [imgs], "flat": [imgs]}。
"""
root = Path(material_root)
result: Dict[str, Any] = {"model": [], "flat": []}
if not root.exists():
return result
mcfg = mark_dirs or {}
cfg = mcfg.get(str(mark)) or mcfg.get("1") or {} # 优先按 mark,其次回退默认 "1"
model_name = str(cfg.get("model_dir") or "").strip()
flat_name = str(cfg.get("flat_dir") or "").strip()
if model_name:
result["model"] = _dir_images(root / model_name, ratio, tolerance)
if flat_name:
result["flat"] = _dir_images(root / flat_name, ratio, tolerance)
# 回退:某类目录未配置时,使用 category 默认目录补充模特图
if not result["model"] and not result["flat"]:
d = root / category
if d.is_dir():
result["model"] = _dir_images(d, ratio, tolerance)
if not result["model"] and not result["flat"]:
for sub in sorted(root.iterdir()):
if not sub.is_dir():
continue
imgs = _dir_images(sub, ratio, tolerance)
if imgs:
result["model"] = imgs
break
return result
def first_available_sku(db_path, basemap_root, spu_code: str) -> Optional[str]: def first_available_sku(db_path, basemap_root, spu_code: str) -> Optional[str]:
"""返回该款号下第一个「本地有底图」的 SKU.code;无则 None。""" """返回该款号下第一个「本地有底图」的 SKU.code;无则 None。"""
for c in list_colors(db_path, spu_code): for c in list_colors(db_path, spu_code):
+117 -21
View File
@@ -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 读取指定 sectionmodel_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
try: last_err: Optional[Exception] = None
image_backend.print(prompt, base_image, out_path, negative, size=size) for attempt in range(1, retries + 1):
paths.append(out_path) if is_kids and kids_scene:
print(f"[seed_shot] 已生成种草图 {i + 1}/{count}: {out_path}" # 童装:场景完整提示词直接作为模板,仅填占位符,不追加任何其他模板
f"(模板={tpl['name']},模特={feat[:14]}…,风格={style_feat[:14]}…)") prompt = render_prompt(kids_scene, cn_title, material, kids_feat, kids_style)
except Exception as e: # noqa: BLE001 tpl_name = "kids_scene"
print(f"[seed_shot] 种草图 {i + 1} 生成失败: {e}") 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 return paths
+2
View File
@@ -33,10 +33,12 @@ class AgentState(TypedDict, total=False):
pinterest_search_terms: List[str] # pinterest_search 产出:LLM 生成的搜索词 pinterest_search_terms: List[str] # pinterest_search 产出:LLM 生成的搜索词
pinterest_images: Dict[str, List[str]] # pinterest_scrape 产出:搜索词 → 爬取图片路径列表 pinterest_images: Dict[str, List[str]] # pinterest_scrape 产出:搜索词 → 爬取图片路径列表
pinterest_briefs: List[Dict[str, Any]] # pinterest_analyze 产出:LLM 分析图片的原始设计简报 pinterest_briefs: List[Dict[str, Any]] # pinterest_analyze 产出:LLM 分析图片的原始设计简报
pinterest_login: Dict[str, Any] # pinterest_scrape 产出:登录态检测结果 {status, detail, ...}
# —— Pinterest 按需搜索循环状态 —— # —— Pinterest 按需搜索循环状态 ——
pinterest_target: int # 目标简报数(= spu_tasks 数量,每款一个设计) pinterest_target: int # 目标简报数(= spu_tasks 数量,每款一个设计)
pinterest_rounds: int # 已搜索轮次 pinterest_rounds: int # 已搜索轮次
pinterest_empty_rounds: int # 连续图片分析无新增简报的轮数(防"逐一取下一张参考图"空转)
pinterest_attempted: List[str] # 本轮已尝试(未持久化)的搜索词,防同轮重复 pinterest_attempted: List[str] # 本轮已尝试(未持久化)的搜索词,防同轮重复
pinterest_pipeline: Any # PinterestPipeline 实例(简报池 + 并发生成线程) pinterest_pipeline: Any # PinterestPipeline 实例(简报池 + 并发生成线程)
+654 -46
View File
@@ -20,12 +20,211 @@ from graph.product import _connect
# 商品轮播图列名关键词(模板存在中/英/日变体,如 商品轮播图1 / Product Carousel Image 1 / 商品カルーセル画像1) # 商品轮播图列名关键词(模板存在中/英/日变体,如 商品轮播图1 / Product Carousel Image 1 / 商品カルーセル画像1)
_CAROUSEL_KW = ("轮播", "carousel", "カルーセル") _CAROUSEL_KW = ("轮播", "carousel", "カルーセル")
# 商品产地:国家简称 → 正式名称(模版要求,如「沙特站」提取为「沙特」但需填「沙特阿拉伯」) # 成分值字典映射:db 成分值 → 女装模板下拉框选项(男装模板选项与 db 值一致,直接保留)。
_COUNTRY_NAME_MAP = { # 女装模板(如 SatVoy 沙特)成分下拉框是「中文+英文」格式(棉Cotton),db 存中文(棉),需映射。
"沙特": "沙特阿拉伯", # 该字典为「内置默认」,运行时可被 config.yaml 顶层 component_map.female 覆盖(缺失/留空回退这里)。
_COMPONENT_FEMALE_MAP = {
"": "棉Cotton",
"聚酯纤维": "聚酯纤维(涤纶)Polyester",
"氨纶": "氨纶Elastane",
"锦纶": "锦纶(尼龙)Polyamide",
"再生聚酯纤维": "聚酯纤维(涤纶)Polyester", # 女装无再生聚酯纤维,归入聚酯纤维
"棉Cotton": "棉Cotton", # 已是女装格式,保持
} }
def _resolve_component_map(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""从 config 读取可配置成分映射(config.component_map.female),缺失/留空回退内置默认。
仅作用于 component_1/2/3material 字段不经过任何映射,原样透传。
"""
if not config:
return dict(_COMPONENT_FEMALE_MAP)
female = None
try:
cm = config.get("component_map") or {}
female = (cm.get("female") or {}) if isinstance(cm, dict) else {}
except Exception: # noqa: BLE001
female = {}
if not isinstance(female, dict) or not female:
return dict(_COMPONENT_FEMALE_MAP)
merged = dict(_COMPONENT_FEMALE_MAP)
merged.update({str(k): v for k, v in female.items() if v not in (None, "")})
return merged
def _map_component(value: Any, gender: Optional[str], cmp_map: Optional[Dict[str, Any]] = None) -> Any:
"""成分值按性别映射(仅 component 字段,不作用于 material):male/None 保留原值;
female 查成分映射表(默认内置、可由 config 覆盖,找不到保留原值)。"""
if gender != "female" or value in (None, ""):
return value
mapping = cmp_map if cmp_map is not None else _COMPONENT_FEMALE_MAP
return mapping.get(value, value)
# 季节值字典映射:db 季节值 → 女装模板下拉框选项(男装模板与 db 值一致,直接保留)。
# 女装模板(如 SatVoy 沙特)季节下拉框用「ALL/全球/所有」等英文+中文格式,db 存「四季」,需映射。
# 该字典为「内置默认」,运行时可被 config.yaml 顶层 season_map.female 覆盖(缺失/留空回退这里)。
_SEASON_FEMALE_MAP = {
"四季": "ALL/全球/所有",
}
# 印花图案值字典映射:db pattern 值 → 女装模板下拉框选项(男装模板与 db 值一致,直接保留)。
# 女装模板遇到 pattern="印花" 时映射为「卡通」;该字典为「内置默认」,
# 运行时可被 config.yaml 顶层 pattern_map.female 覆盖(缺失/留空回退这里)。
_PATTERN_FEMALE_MAP = {
"印花": "卡通",
}
def _resolve_pattern_map(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""从 config 读取可配置图案映射(config.pattern_map.female),缺失/留空回退内置默认。
仅作用于 spu.pattern 字段(女装模板时映射,男装/None 保留原值)。
"""
if not config:
return dict(_PATTERN_FEMALE_MAP)
female = None
try:
pm = config.get("pattern_map") or {}
female = (pm.get("female") or {}) if isinstance(pm, dict) else {}
except Exception: # noqa: BLE001
female = {}
if not isinstance(female, dict) or not female:
return dict(_PATTERN_FEMALE_MAP)
merged = dict(_PATTERN_FEMALE_MAP)
merged.update({str(k): v for k, v in female.items() if v not in (None, "")})
return merged
def _map_pattern(value: Any, gender: Optional[str], pattern_map: Optional[Dict[str, Any]] = None) -> Any:
"""图案值按性别映射(仅 spu.pattern 字段):male/None 保留原值;
female 查图案映射表(默认内置、可由 config 覆盖,找不到保留原值),如「印花」→「卡通」。"""
if gender != "female" or value is None:
return value
v = str(value).strip()
mapping = pattern_map if pattern_map is not None else _PATTERN_FEMALE_MAP
return mapping.get(v, value)
def _resolve_season_map(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""从 config 读取可配置季节映射(config.season_map.female),缺失/留空回退内置默认。
仅作用于 spu.season 字段(女装模板时映射,男装/None 保留原值)。
"""
if not config:
return dict(_SEASON_FEMALE_MAP)
female = None
try:
sm = config.get("season_map") or {}
female = (sm.get("female") or {}) if isinstance(sm, dict) else {}
except Exception: # noqa: BLE001
female = {}
if not isinstance(female, dict) or not female:
return dict(_SEASON_FEMALE_MAP)
merged = dict(_SEASON_FEMALE_MAP)
merged.update({str(k): v for k, v in female.items() if v not in (None, "")})
return merged
def _map_season(value: Any, gender: Optional[str], season_map: Optional[Dict[str, Any]] = None) -> Any:
"""季节值按性别映射(仅 spu.season 字段):male/None 保留原值;
female 查季节映射表(默认内置、可由 config 覆盖,找不到保留原值)。"""
if gender != "female" or value in (None, ""):
return value
mapping = season_map if season_map is not None else _SEASON_FEMALE_MAP
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]:
"""通过表头解析模版「类目」判断男装/女装:类目含「男」→male;含「女」→female;都不含→None。
用于成分值映射(女装模板下拉框是「中文+英文」格式,需字典映射;男装保留 db 值)。"""
try:
from graph.seed_shot import gender_from_category, read_template_category
return gender_from_category(read_template_category(template_path))
except Exception: # noqa: BLE001
return None
def _size_rank(size) -> tuple: def _size_rank(size) -> tuple:
"""把尺码字符串转成可排序 rank(从小到大)。 """把尺码字符串转成可排序 rank(从小到大)。
@@ -49,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
@@ -204,22 +406,69 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
router.ws.cell(row, car1, cc["url"]) # 该颜色轮播图1 router.ws.cell(row, car1, cc["url"]) # 该颜色轮播图1
def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str, def _build_spu_row(spu: Dict[str, Any], spu_code: str,
color: Optional[str] = None) -> Dict[str, Any]: color: Optional[str] = None,
"""构造一行 SPU(固定字段:SKC货号=code、风格=休闲、商品产地=经营站点;多颜色时用色值列区分)。""" fabric_headers: Optional[List[str]] = None,
gender: Optional[str] = None,
component_map: Optional[Dict[str, Any]] = None,
season_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,
oss_code: str = "",
model_info: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
"""构造一行 SPU(固定字段:SKC货号=当前生成货号、风格=休闲、商品产地=中国大陆、产地省份=广东省;多颜色时用色值区分)。
fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。
component_1/2/3 按性别映射(gender=female 时查成分映射表 component_map,男装/None 保留原值;
material 字段不经过任何映射,原样透传)。
season 按性别映射(gender=female 时查季节映射表 season_map,如「四季」→「ALL/全球/所有」)。
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货号": spu_code, # code 路由为 SKC货号(用户要求 "SKC货号": oss_code or spu_code, # SKC货号 = 当前生成货号 oss_code(无则回退 spu_code
"风格": "休闲", # style 路由为"休闲"(用户要求) "风格": "休闲", # style 路由为"休闲"(用户要求)
"商品产地": origin_province, # 产地省份不用填,经营站点填到「商品产地」 "商品产地": "中国大陆", # 所有国家统一「中国大陆」,不读经营站点/不做字典匹配(用户要求)
"产地省份": "广东省", # 新增:精确匹配「产地省份」列,统一填「广东省」(用户要求)
"款式来源": "现货款", # SPU商品属性-款式来源 统一填「现货款」(用户要求) "款式来源": "现货款", # SPU商品属性-款式来源 统一填「现货款」(用户要求)
} }
if color: if color:
row["色值(主规格)"] = color row["色值(主规格)"] = color
for dbk, header in SPU_MAP.items(): for dbk, header in SPU_MAP.items():
v = spu.get(dbk) v = spu.get(dbk)
if dbk in ("component_1", "component_2", "component_3"):
v = _map_component(v, gender, component_map)
elif dbk == "season":
v = _map_season(v, gender, season_map)
elif dbk == "pattern":
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")
if fabric not in (None, "") and fabric_headers:
for h in fabric_headers:
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
@@ -235,19 +484,243 @@ def _find_bust_headers(router) -> List[str]:
return hits or ["胸围全围(cm"] return hits or ["胸围全围(cm"]
def _find_fabric_headers(router) -> List[str]:
"""定位所有「面料弹性」列(SPU商品属性-面料弹性…模糊匹配);匹配到多个时全部填 spu.fabric。"""
return [str(k) for k in router.column_map if "面料弹性" in str(k)]
def _find_sa_size_headers(router) -> List[str]:
"""定位所有「沙特阿拉伯码」列(尺码表-沙特阿拉伯码…模糊匹配);匹配到多个时全部填 sku.size。"""
return [str(k) for k in router.column_map if "沙特阿拉伯码" in str(k)]
def _find_suggested_price_headers(router) -> List[str]:
"""定位「建议售价」列(不含「单位」);匹配到多个时全部返回。"""
return [str(k) for k in router.column_map if "建议售价" in str(k) and "单位" not in str(k)]
def _find_suggested_unit_headers(router) -> List[str]:
"""定位「建议售价单位」列。"""
return [str(k) for k in router.column_map if "建议售价单位" in str(k)]
def _read_range_values(ws, ref: str) -> Optional[set]:
"""读取单元格/区域内的非空值集合;解析失败返回 None。"""
try:
cells = ws[ref]
except Exception: # noqa: BLE001
return None
opts = []
if isinstance(cells, (list, tuple)):
for row in cells:
for cell in row:
if cell.value not in (None, ""):
opts.append(str(cell.value).strip())
else:
if cells.value not in (None, ""):
opts.append(str(cells.value).strip())
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" / "SML"
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]:
"""读取「尺码」列的 dataValidation 下拉选项集合;无下拉/非 list/无值 返回 None。
仅作「尺码下拉受限时过滤 SKU 行」用:sku.size 不在下拉选项内则该 SKU 行跳过。
支持下拉直接内联("S,M,L")、引用区域("=Sheet!$A$1:$A$5")、动态引用(INDIRECT(...))。
动态引用无法解析(INDIRECT 指向空单元格/公式/跨表)→ 返回 None(不启用过滤,避免误杀全部 SKU 行)。"""
import re
from openpyxl.utils import get_column_letter
col = router.column_map.get("尺码") or router.column_map.get("商品规格-尺码")
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(...):尝试解析引用的单元格/区域内容;无法解析 → 不过滤
m = re.match(r"^=?INDIRECT\((.+)\)$", f, re.IGNORECASE)
if m:
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
# 引用区域(=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(router.ws, ref)
return opts if opts else None
# 内联列表("S,M,L" / "S;M;L" / "SML"
sep = "" if "" in f else (";" if ";" in f else ",")
opts = [x.strip() for x in f.split(sep) if x.strip()]
return set(opts) if opts else None
return None
def _find_sku_category_headers(router) -> List[str]:
"""定位「SKU分类」列(模糊匹配,同建议售价检测方式);匹配到多个时全部返回。"""
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]:
"""定位「SKU数量」列(排除「SKU数量单位」列);匹配到多个时全部返回。"""
return [str(k) for k in router.column_map if "SKU数量" in str(k) and "单位" not in str(k)]
def _find_sku_qty_unit_headers(router) -> List[str]:
"""定位「SKU数量单位」列。"""
return [str(k) for k in router.column_map if "SKU数量单位" in str(k)]
def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color: str, def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color: str,
warehouses: List[str], markup_percent: float = 0.0, warehouses: List[str], markup_percent: float = 0.0,
multi: bool = True, price_header: str = "申报价格-日本站", multi: bool = True, price_header: str = "申报价格-日本站",
bust_headers: Optional[List[str]] = None, bust_headers: Optional[List[str]] = None,
price_headers: Optional[List[str]] = None) -> Dict[str, Any]: price_headers: Optional[List[str]] = None,
"""构造一行 SKU(固定字段:SPU货号、SKC货号=sku.code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。 sa_size_headers: Optional[List[str]] = None,
suggested_price_ratio: float = 0.0,
suggested_price_headers: Optional[List[str]] = None,
suggested_unit_headers: Optional[List[str]] = None,
sku_category_headers: Optional[List[str]] = None,
sku_qty_headers: Optional[List[str]] = None,
sku_qty_unit_headers: Optional[List[str]] = None,
oss_code: str = "",
size_options: Optional[set] = None) -> Dict[str, Any]:
"""构造一行 SKU(固定字段:SPU货号、SKC货号=当前生成货号 oss_code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。
价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100) 价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100)
预先填好。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。 预先填好。建议售价(suggested_price_headers 列,模板「建议售价」必填时才填)= 申报价格 × (1+suggested_price_ratio/100)
填了建议售价同时填「建议售价单位」=CNY。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。
size 填「尺码」列 + 所有「沙特阿拉伯码」列(sa_size_headers,如 尺码表-沙特阿拉伯码,检测到才填)。
规格类型2 统一填「尺码」两个字(不是 size 参数值)。""" 规格类型2 统一填「尺码」两个字(不是 size 参数值)。"""
row: Dict[str, Any] = { row: Dict[str, Any] = {
"基础信息-商品层级": "sku", "基础信息-商品层级": "sku",
"SPU货号": spu_code, "SPU货号": spu_code,
"SKC货号": sk.get("code") or sc, # SKC货号 = SKU 的 code(款号-颜色编码 "SKC货号": oss_code or spu_code, # SKC货号 = 当前生成货号 oss_code(无则回退 spu_code
"色值(主规格)": color, "色值(主规格)": color,
"规格类型2": "尺码", # 规格类型2 统一填「尺码」(不填 size 值) "规格类型2": "尺码", # 规格类型2 统一填「尺码」(不填 size 值)
"币种": "CNY", "币种": "CNY",
@@ -256,6 +729,8 @@ def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color:
price_headers = [price_header] price_headers = [price_header]
if bust_headers is None: if bust_headers is None:
bust_headers = ["胸围全围(cm"] bust_headers = ["胸围全围(cm"]
if sa_size_headers is None:
sa_size_headers = []
for j, w in enumerate(warehouses, start=1): for j, w in enumerate(warehouses, start=1):
row[f"发货仓{j}"] = w row[f"发货仓{j}"] = w
row[f"发货仓{j}库存"] = 200 row[f"发货仓{j}库存"] = 200
@@ -275,9 +750,32 @@ def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color:
v = round(float(v) * (1 + markup_percent / 100), 2) # 申报价格 = price × (1+加价%) v = round(float(v) * (1 + markup_percent / 100), 2) # 申报价格 = price × (1+加价%)
for h in price_headers: for h in price_headers:
row[h] = v row[h] = v
# 建议售价 = 申报价格 × (1+建议售价比例%);统一必填,填了同时填单位 CNY
if suggested_price_headers and suggested_price_ratio > 0:
suggested = round(v * (1 + suggested_price_ratio / 100), 2)
for h in suggested_price_headers:
row[h] = suggested
for uh in suggested_unit_headers:
row[uh] = "CNY"
continue
if dbk == "size":
if v in (None, ""):
continue
if size_options is not None and v not in size_options:
return None # 尺码不在模板下拉框选项内,跳过该 SKU 行
row[header] = v
for h in sa_size_headers:
row[h] = v
continue continue
if v not in (None, ""): if v not in (None, ""):
row[header] = v row[header] = v
# SKU 分类/数量/数量单位:模板检测到这些列(且非「非必填」)时统一填固定值(单品 / 1 / 件)
for h in (sku_category_headers or []):
row[h] = "单品"
for h in (sku_qty_headers or []):
row[h] = 1
for h in (sku_qty_unit_headers or []):
row[h] = ""
return row return row
@@ -315,13 +813,23 @@ SPU_MAP: Dict[str, str] = {
"target_audience": "适用人群", "target_audience": "适用人群",
"season": "季节", "season": "季节",
"is_transparent": "是否透明", "is_transparent": "是否透明",
"layout": "版型",
"weaving_method": "织造方式", "weaving_method": "织造方式",
"printing_type": "印花类型", "printing_type": "印花类型",
"fabric_texture_1": "面料纹理1", "fabric_texture_1": "面料纹理1",
"fabric_weight_1": "面料克重1g/m²)", "fabric_weight_1": "面料克重1g/m²)",
"fabric_weight_unit_1": "面料克重1g/m²)单位", "fabric_weight_unit_1": "面料克重1g/m²)单位",
"lining_texture": "里料纹理", "lining_texture": "里料纹理",
"placket_type": "SPU商品属性-门襟类型",
"sleeve_type": "SPU商品属性-袖型",
"sleeve_length_type": "SPU商品属性-袖长",
"breast_pad": "SPU商品属性-胸垫",
"layout": "SPU商品属性-版型",
"silhouette": "SPU商品属性-廓形",
"length_type": "SPU商品属性-长度",
"belt": "SPU商品属性-腰带",
"occasion": "SPU商品属性-场合",
"scene": "SPU商品属性-场景",
"hemline_shape": "SPU商品属性-下摆形状",
} }
# db SKU 字段 -> 上传模板列名 # db SKU 字段 -> 上传模板列名
@@ -350,12 +858,10 @@ def _read_spu(db_path, spu_code: str) -> Optional[Dict[str, Any]]:
def _read_meta(router) -> tuple: def _read_meta(router) -> tuple:
"""读模板顶头元信息:经营站点、发货仓(按标签名定位,不依赖固定行列)。 """读模板顶头元信息:发货仓(按标签名精准定位,不依赖固定行列)。
返回 (origin_province, warehouses) 返回 warehouses:发货仓按「、」分隔的列表(如「名古屋仓、inkreach——东京」→ 2 个)。
- origin_province:经营站点去掉末尾「站」(如「日本站」→「日本」) 商品产地/产地省份已固定为「中国大陆/广东省」写死在 _build_spu_row,不再读取经营站点。"""
- warehouses:发货仓按「、」分隔的列表(如「名古屋仓、inkreach——东京」→ 2 个)
"""
ws = router.ws ws = router.ws
top = max(1, router.group_row - 1) # 元信息区位于分组行之前 top = max(1, router.group_row - 1) # 元信息区位于分组行之前
@@ -366,16 +872,12 @@ def _read_meta(router) -> tuple:
return str(ws.cell(row + 1, col).value or "").strip() return str(ws.cell(row + 1, col).value or "").strip()
return "" return ""
site = _val("经营站点")
raw = _val("发货仓") raw = _val("发货仓")
if not site and not raw: if not raw:
# 回退:旧版固定位置(第2行第1/2列) # 回退:旧版固定位置(第2行第2列=发货仓
site = str(ws.cell(2, 1).value or "").strip()
raw = str(ws.cell(2, 2).value or "").strip() raw = str(ws.cell(2, 2).value or "").strip()
origin_province = site[:-1] if site.endswith("") else site
origin_province = _COUNTRY_NAME_MAP.get(origin_province, origin_province) # 简称→正式名称(如 沙特→沙特阿拉伯)
warehouses = [w.strip() for w in raw.split("") if w.strip()] warehouses = [w.strip() for w in raw.split("") if w.strip()]
return origin_province, warehouses return warehouses
def _read_skus(db_path, spu_code: str, sku_code: str) -> List[Dict[str, Any]]: def _read_skus(db_path, spu_code: str, sku_code: str) -> List[Dict[str, Any]]:
@@ -408,7 +910,7 @@ def _import_router(template_dir: str) -> None:
def _insert_product_block( def _insert_product_block(
router, db_path, spu_code, sku_code, router, db_path, spu_code, sku_code,
origin_province, warehouses, price_headers, warehouses, price_headers,
markup_percent: float = 0.0, images: Optional[List[str]] = None, markup_percent: float = 0.0, images: Optional[List[str]] = None,
spu_per_color: bool = True, spu_per_color: bool = True,
oss_code: str = "", cn_title: str = "", en_title: str = "", ja_title: str = "", oss_code: str = "", cn_title: str = "", en_title: str = "", ja_title: str = "",
@@ -416,6 +918,20 @@ def _insert_product_block(
composite_urls: Optional[List[Dict[str, Any]]] = None, composite_urls: Optional[List[Dict[str, Any]]] = None,
seed_shot_urls: Optional[List[str]] = None, seed_shot_urls: Optional[List[str]] = None,
bust_headers: Optional[List[str]] = None, bust_headers: Optional[List[str]] = None,
fabric_headers: Optional[List[str]] = None,
sa_size_headers: Optional[List[str]] = None,
gender: Optional[str] = None,
component_map: Optional[Dict[str, Any]] = None,
season_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_headers: Optional[List[str]] = None,
suggested_unit_headers: Optional[List[str]] = None,
sku_category_headers: Optional[List[str]] = None,
sku_qty_headers: Optional[List[str]] = None,
sku_qty_unit_headers: Optional[List[str]] = None,
) -> List[int]: ) -> List[int]:
"""在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。 """在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。
@@ -435,17 +951,25 @@ 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 行)
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 填充)
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分) # + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
block_rows.append(router.insert( block_rows.append(router.insert(
_build_spu_row(spu, spu_code, origin_province), _build_spu_row(spu, spu_code, fabric_headers=fabric_headers,
gender=gender, component_map=component_map,
season_map=season_map, pattern_map=pattern_map,
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):
@@ -455,12 +979,21 @@ def _insert_product_block(
# 该颜色全部尺码 SKUSKU 行 SPU货号/SKU货号=spu_code,色值区分) # 该颜色全部尺码 SKUSKU 行 SPU货号/SKU货号=spu_code,色值区分)
for i, sk in enumerate(skus): for i, sk in enumerate(skus):
size = sk.get("size") or f"{i+1}" size = sk.get("size") or f"{i+1}"
block_rows.append(router.insert( row = _build_sku_row(spu_code, sc, sk, size, color, warehouses,
_build_sku_row(spu_code, sc, sk, size, color, warehouses, markup_percent=markup_percent, multi=True,
markup_percent=markup_percent, multi=True, bust_headers=bust_headers, price_headers=price_headers,
bust_headers=bust_headers, price_headers=price_headers), sa_size_headers=sa_size_headers,
spu_code=spu_code, match="exact", suggested_price_ratio=suggested_price_ratio,
)) suggested_price_headers=suggested_price_headers,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
sku_qty_unit_headers=sku_qty_unit_headers,
oss_code=oss_code, size_options=size_options)
if row is None:
continue
block_rows.append(router.insert(row, spu_code=spu_code, match="exact"))
# 轮播图:首色 SKU 行轮播图1 = 生成首图;SKU 行按色值填 db url/生成图 # 轮播图:首色 SKU 行轮播图1 = 生成首图;SKU 行按色值填 db url/生成图
if ci == 0 and images: if ci == 0 and images:
@@ -474,18 +1007,33 @@ def _insert_product_block(
else: else:
# 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分) # 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
block_rows.append(router.insert( block_rows.append(router.insert(
_build_spu_row(spu, spu_code, origin_province), match="exact")) _build_spu_row(spu, spu_code, fabric_headers=fabric_headers,
gender=gender, component_map=component_map,
season_map=season_map, pattern_map=pattern_map,
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]
color = first.get("color") or sc color = first.get("color") or sc
for i, sk in enumerate(skus): for i, sk in enumerate(skus):
size = sk.get("size") or f"{i+1}" size = sk.get("size") or f"{i+1}"
block_rows.append(router.insert( row = _build_sku_row(spu_code, sc, sk, size, color, warehouses,
_build_sku_row(spu_code, sc, sk, size, color, warehouses, markup_percent=markup_percent, multi=multi_variant,
markup_percent=markup_percent, multi=multi_variant, bust_headers=bust_headers, price_headers=price_headers,
bust_headers=bust_headers, price_headers=price_headers), sa_size_headers=sa_size_headers,
)) suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
sku_qty_unit_headers=sku_qty_unit_headers,
oss_code=oss_code, size_options=size_options)
if row is None:
continue
block_rows.append(router.insert(row))
sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else [] sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else []
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs) _fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
@@ -523,6 +1071,12 @@ def export_product(
seed_shot_urls: Optional[List[str]] = None, seed_shot_urls: Optional[List[str]] = None,
append_to: str = "", append_to: str = "",
markup_percent: float = 0.0, markup_percent: float = 0.0,
suggested_price_ratio: float = 0.0,
component_map: Optional[Dict[str, Any]] = None,
season_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:
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。 """生成商品上传"已填写"模板(支持多产品合并到同一文件)。
@@ -548,18 +1102,42 @@ def export_product(
# append_to:合并模式从已有输出文件继续追加(一次任务多产品填一个模板) # append_to:合并模式从已有输出文件继续追加(一次任务多产品填一个模板)
router = TemplateRouter(append_to if append_to else template_path) router = TemplateRouter(append_to if append_to else template_path)
try: try:
origin_province, warehouses = _read_meta(router) warehouses = _read_meta(router)
price_headers = _find_price_headers(router) # 申报价格列(美站/日站/英站…模糊匹配,多个全填) price_headers = _find_price_headers(router) # 申报价格列(美站/日站/英站…模糊匹配,多个全填)
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填) bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填)
sa_size_headers = _find_sa_size_headers(router) # 沙特阿拉伯码列(尺码表-沙特阿拉伯码…检测到才填)
gender = _template_gender(template_path) # 类目含「男」→male /「女」→female(成分值映射用)
suggested_price_headers = _find_suggested_price_headers(router) # 建议售价列(不含单位)
suggested_unit_headers = _find_suggested_unit_headers(router) # 建议售价单位列
sku_category_headers = _find_sku_category_headers(router) # SKU分类列(存在即填默认值「单品」)
sku_qty_headers = _find_sku_qty_headers(router) # SKU数量列(存在即填 1
sku_qty_unit_headers = _find_sku_qty_unit_headers(router) # SKU数量单位列(存在即填「件」)
_insert_product_block(router, db_path, spu_code, sku_code, _insert_product_block(router, db_path, spu_code, sku_code,
origin_province, warehouses, price_headers, warehouses, price_headers,
markup_percent=markup_percent, images=images, markup_percent=markup_percent, images=images,
spu_per_color=spu_per_color, spu_per_color=spu_per_color,
oss_code=oss_code, cn_title=cn_title, en_title=en_title, oss_code=oss_code, cn_title=cn_title, en_title=en_title,
ja_title=ja_title, es_title=es_title, ja_title=ja_title, es_title=es_title,
composite_urls=composite_urls, composite_urls=composite_urls,
seed_shot_urls=seed_shot_urls, seed_shot_urls=seed_shot_urls,
bust_headers=bust_headers) bust_headers=bust_headers,
fabric_headers=fabric_headers,
sa_size_headers=sa_size_headers,
gender=gender,
component_map=component_map,
season_map=season_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_headers=suggested_price_headers,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
sku_qty_unit_headers=sku_qty_unit_headers)
out = router.save(out_path) out = router.save(out_path)
return Path(out) return Path(out)
finally: finally:
@@ -576,6 +1154,12 @@ def export_products(
template_path: str, template_path: str,
out_path: str, out_path: str,
markup_percent: float = 0.0, markup_percent: float = 0.0,
suggested_price_ratio: float = 0.0,
component_map: Optional[Dict[str, Any]] = None,
season_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:
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。 """批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
@@ -588,15 +1172,24 @@ def export_products(
router = TemplateRouter(template_path) router = TemplateRouter(template_path)
try: try:
origin_province, warehouses = _read_meta(router) warehouses = _read_meta(router)
price_headers = _find_price_headers(router) # 申报价格列(美站/日站/英站…模糊匹配,多个全填) price_headers = _find_price_headers(router) # 申报价格列(美站/日站/英站…模糊匹配,多个全填)
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填) bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填)
sa_size_headers = _find_sa_size_headers(router) # 沙特阿拉伯码列(尺码表-沙特阿拉伯码…检测到才填)
gender = _template_gender(template_path) # 类目含「男」→male /「女」→female(成分值映射用)
suggested_price_headers = _find_suggested_price_headers(router) # 建议售价列(不含单位)
suggested_unit_headers = _find_suggested_unit_headers(router) # 建议售价单位列
sku_category_headers = _find_sku_category_headers(router) # SKU分类列(存在即填默认值「单品」)
sku_qty_headers = _find_sku_qty_headers(router) # SKU数量列(存在即填 1
sku_qty_unit_headers = _find_sku_qty_unit_headers(router) # SKU数量单位列(存在即填「件」)
for r in products: for r in products:
try: try:
_insert_product_block( _insert_product_block(
router, db_path, r.get("spu_code", ""), router, db_path, r.get("spu_code", ""),
r.get("sku_codes") or r.get("sku_code") or "", r.get("sku_codes") or r.get("sku_code") or "",
origin_province, warehouses, price_headers, warehouses, price_headers,
markup_percent=markup_percent, markup_percent=markup_percent,
images=r.get("images"), images=r.get("images"),
spu_per_color=bool(r.get("spu_per_color", True)), spu_per_color=bool(r.get("spu_per_color", True)),
@@ -608,6 +1201,21 @@ def export_products(
composite_urls=r.get("composite_urls"), composite_urls=r.get("composite_urls"),
seed_shot_urls=r.get("seed_shot_urls"), seed_shot_urls=r.get("seed_shot_urls"),
bust_headers=bust_headers, bust_headers=bust_headers,
fabric_headers=fabric_headers,
sa_size_headers=sa_size_headers,
gender=gender,
component_map=component_map,
season_map=season_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_headers=suggested_price_headers,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
sku_qty_unit_headers=sku_qty_unit_headers,
) )
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}") print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}")
+57 -2
View File
@@ -8,15 +8,69 @@
剔除非法记录并记录原因,保证下游拿到的数据"形状正确" 剔除非法记录并记录原因,保证下游拿到的数据"形状正确"
""" """
import functools import functools
import threading
import traceback import traceback
from typing import Any, Dict, List from typing import Any, Dict, List
class ThreadSafeErrors:
"""线程安全的错误收集器:并发节点(compose/product 等)内 append 错误用。
避免多个 worker 线程直接写共享 list 造成竞态;主线程统一合并到 state['errors']。
"""
def __init__(self) -> None:
self._lock = threading.Lock()
self._items: List[Dict[str, Any]] = []
def append(self, item: Dict[str, Any]) -> None:
with self._lock:
self._items.append(item)
def __iter__(self):
with self._lock:
return iter(list(self._items))
def __len__(self) -> int:
with self._lock:
return len(self._items)
def _mark_fatal_503(state: Dict[str, Any], exc: Exception) -> None:
"""致命图像服务错误(503 / No available compatible accounts)→ 标记提前终止,不静默吞掉。
with_fallback 原本把所有异常都转成一条错误记录并继续,导致致命的 503 被「吞掉」:
路由看不到终止信号,任务会继续做无意义的搜索/分析(重试必然失败)。
检测到致命 503 时同步标记 Pinterest 流水线终止(record_503 + abort_unfinished),
让 _pinterest_route 短路到 pinterest_finalize → template_export(合成模板,保留已完成产品)。
非 Pinterest 节点无流水线对象时,此函数为空操作(不改变原有兜底行为)。
"""
if not _is_fatal_image_error(exc):
return
pipe = state.get("pinterest_pipeline")
if pipe is not None and hasattr(pipe, "record_503") and hasattr(pipe, "abort_unfinished"):
try:
pipe.record_503()
pipe.abort_unfinished()
except Exception: # noqa: BLE001
pass
def _is_fatal_image_error(exc: Exception) -> bool:
"""判断异常是否为致命图像服务错误(503 / 账户不可用)。"""
try:
from graph.pinterest_pipeline import PinterestPipeline
return bool(PinterestPipeline.is_fatal_503(exc))
except Exception: # noqa: BLE001
return False
def with_fallback(node_name: str): def with_fallback(node_name: str):
"""装饰器:捕获节点异常,转为 state['errors'] 中的一条记录,返回空更新。 """装饰器:捕获节点异常,转为 state['errors'] 中的一条记录,返回空更新。
节点内部仍建议自己做精细兜底(降级/默认),with_fallback 是最后一道保险: 节点内部仍建议自己做精细兜底(降级/默认),with_fallback 是最后一道保险:
任何未预料的异常都不会让整张图中断。 任何未预料的异常都不会让整张图中断。唯一例外——致命图像服务错误(503/账户不可用)
不静默吞掉:会同步标记流水线终止,让任务提前收尾合成模板(见 _mark_fatal_503)。
""" """
def deco(fn): def deco(fn):
@@ -34,6 +88,8 @@ def with_fallback(node_name: str):
} }
errors = list(state.get("errors") or []) errors = list(state.get("errors") or [])
errors.append(err) errors.append(err)
# 致命 503:不静默吞掉,标记流水线终止(路由据此短路到收尾合成模板)
_mark_fatal_503(state, e)
# 只更新 errors,其它字段保持上一节点结果,下游继续 # 只更新 errors,其它字段保持上一节点结果,下游继续
return {"errors": errors} return {"errors": errors}
@@ -82,7 +138,6 @@ def validate_brief(b: Dict[str, Any]) -> Dict[str, Any]:
b.setdefault("color_palette", "balanced modern palette") b.setdefault("color_palette", "balanced modern palette")
b.setdefault("composition", "centered emblem with balanced negative space") b.setdefault("composition", "centered emblem with balanced negative space")
b.setdefault("concept", b.get("topic", "")) b.setdefault("concept", b.get("topic", ""))
b.setdefault("negative_prompt", "")
b.setdefault("image_prompt", "") b.setdefault("image_prompt", "")
b.setdefault("wearable_prompt", "") b.setdefault("wearable_prompt", "")
b.setdefault("composite_prompt", "") b.setdefault("composite_prompt", "")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

+100 -4
View File
@@ -5,7 +5,9 @@ import os
import random import random
import socket import socket
import sys import sys
import time
import urllib.request import urllib.request
from typing import Any, Dict
from urllib.parse import urlparse, quote from urllib.parse import urlparse, quote
import aiohttp import aiohttp
@@ -218,6 +220,80 @@ def _session_user_data_dir() -> str:
return ud return ud
def check_login_state() -> Dict[str, Any]:
"""静态检测 Pinterest 登录态(不启动 Chrome,只读持久化 cookies 数据库)。
通过检查 .chrome_session/User Data 里的 Chrome cookies 数据库:
- 无目录 / 无 cookies 文件 → no_session(从未登录过)
- 有 cookies 但无 Pinterest 域 cookie → logged_out(未登录过 Pinterest
- 有 Pinterest 域 cookie 但缺关键登录 cookie_auth/csrftoken)或已过期 → logged_out
- 含 _auth 且未过期 → logged_in
- 数据库无法解析 → unknown(调用方走运行时检测兜底)
注意:Chrome 在 Windows 上对 cookie 值做 DPAPI 加密,这里只判断
cookie 的存在性 / 域名 / 过期时间,不解析密文值。
"""
ud = _session_user_data_dir()
out: Dict[str, Any] = {
"status": "unknown",
"session_dir": ud,
"cookies_file": "",
"pinterest_cookies": 0,
"has_auth_cookie": False,
"expired": False,
"detail": "",
}
if not os.path.isdir(ud):
out["status"] = "no_session"
out["detail"] = "未找到 Chrome 登录态目录(从未登录过 Pinterest)"
return out
cookies_file = None
for rel in ("Default/Network/Cookies", "Default/Cookies"):
cand = os.path.join(ud, rel)
if os.path.isfile(cand) and os.path.getsize(cand) > 0:
cookies_file = cand
break
if not cookies_file:
out["status"] = "no_session"
out["detail"] = "未找到 cookies 数据库(从未登录过 Pinterest)"
return out
out["cookies_file"] = cookies_file
try:
import sqlite3
conn = sqlite3.connect(f"file:{cookies_file}?mode=ro&immutable=1", uri=True)
try:
rows = conn.execute(
"SELECT host_key, name, expires_utc FROM cookies "
"WHERE host_key LIKE '%pinterest.com%' OR host_key LIKE '%pinimg.com%'"
).fetchall()
finally:
conn.close()
except Exception as e: # noqa: BLE001
out["detail"] = f"cookies 读取失败: {e}"
return out
out["pinterest_cookies"] = len(rows)
# WebKit epoch1601-01-01 UTC)微秒偏移;expires_utc=0 表示会话 cookie(不判过期)
WEBKIT_EPOCH_US = 11644473600000000
now_us = int(time.time() * 1_000_000)
auth_names = ("_auth", "csrftoken", "sessionFunnelEventLogged")
for host, name, exp in rows:
if name in auth_names:
out["has_auth_cookie"] = True
if exp and exp != 0 and exp < now_us:
out["expired"] = True
if out["has_auth_cookie"] and not out["expired"]:
out["status"] = "logged_in"
out["detail"] = f"检测到 Pinterest 登录态({len(rows)} 个 Pinterest 域 cookie,含 _auth"
elif out["pinterest_cookies"] > 0:
out["status"] = "logged_out"
out["detail"] = (f"存在 {len(rows)} 个 Pinterest 域 cookie,但缺少有效登录 cookie"
f"(可能已过期或退出登录)")
else:
out["status"] = "logged_out"
out["detail"] = "cookies 数据库中无 Pinterest 域 cookie(未登录过 Pinterest"
return out
async def download_image(session: aiohttp.ClientSession, url: str, idx: int, async def download_image(session: aiohttp.ClientSession, url: str, idx: int,
sem: asyncio.Semaphore, save_dir: str, proxy: str | None): sem: asyncio.Semaphore, save_dir: str, proxy: str | None):
filename = get_filename_from_url(url, idx) filename = get_filename_from_url(url, idx)
@@ -284,7 +360,7 @@ async def human_scroll(page: Page, distance: int):
await page.wait_for_timeout(random.randint(30, 100)) # 滚轮事件间隔 await page.wait_for_timeout(random.randint(30, 100)) # 滚轮事件间隔
async def _ensure_logged_in(page: Page, search_url: str) -> bool: async def _ensure_logged_in(page: Page, search_url: str, login_wait: bool = False) -> bool:
"""独立登录检查方法:在已跳到搜索页的前提下确认 Pinterest 已登录。 """独立登录检查方法:在已跳到搜索页的前提下确认 Pinterest 已登录。
判定策略(避免误判): 判定策略(避免误判):
@@ -292,7 +368,9 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
才认定未登录;否则默认已登录(不依赖可能不匹配的已登录选择器)。 才认定未登录;否则默认已登录(不依赖可能不匹配的已登录选择器)。
- Pinterest 是 SPAgoto 后需等待渲染,否则瞬间误判未登录。 - Pinterest 是 SPAgoto 后需等待渲染,否则瞬间误判未登录。
- 已登录 → 立即返回 True,走直路。 - 已登录 → 立即返回 True,走直路。
- 未登录(被弹回登录墙)→ 回退首页提示手动登录一次,登录成功后返回 True。 - 未登录
login_wait=True → 回退首页提示手动登录一次,登录成功后返回 True;
login_wait=False → 不阻塞,直接返回 False(由调用方跳过并告警)。
- 一直未登录(用户关窗口/放弃)→ 返回 False。 - 一直未登录(用户关窗口/放弃)→ 返回 False。
""" """
async def _has_login_wall() -> bool: async def _has_login_wall() -> bool:
@@ -327,6 +405,13 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
print("✅ 已检测到登录态,直接开始搜索") print("✅ 已检测到登录态,直接开始搜索")
return True return True
# 未登录:默认不阻塞(跳过并告警);login_wait=True 才回退首页等手动登录
if not login_wait:
print("⚠️ 未检测到 Pinterest 登录态(cookie 缺失或已过期)。"
"请先在 pinterest_scraper/.chrome_session 目录登录 Pinterest 一次,"
"或设置 pinterest.login_wait=true 让程序自动等待手动登录。")
return False
# 未登录(搜索页被弹回登录墙):回退首页,提示手动登录一次,成功后立即继续 # 未登录(搜索页被弹回登录墙):回退首页,提示手动登录一次,成功后立即继续
print("⚠️ 当前未登录(搜索页被拦截)。请在弹出的浏览器窗口中手动登录,登录成功后将自动继续……") print("⚠️ 当前未登录(搜索页被拦截)。请在弹出的浏览器窗口中手动登录,登录成功后将自动继续……")
try: try:
@@ -351,7 +436,16 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
async def scrape(keyword: str, count: int, headless: bool = False, async def scrape(keyword: str, count: int, headless: bool = False,
proxy: str | None = None, proxy: str | None = None,
cdp_url: str | None = None) -> set[str]: cdp_url: str | None = None,
login_wait: bool = False,
known_urls: set[str] | None = None) -> set[str]:
"""按关键词滚动采集 Pinterest 图片 URL。
known_urls:该关键词历史已采集 URL 集合(跨运行持久化,见 scraper.scrape_pinterest)。
滚动时命中 known_urls 的旧图直接跳过(不计入 count),从而在重复搜索同一关键词时
自动往下拉采集新图,而不是反复下载顶部旧图。
"""
known = set(known_urls or [])
imgs_url: set[str] = set() imgs_url: set[str] = set()
async with async_playwright() as p: async with async_playwright() as p:
if cdp_url: if cdp_url:
@@ -389,7 +483,7 @@ async def scrape(keyword: str, count: int, headless: bool = False,
await page.goto(search_url, wait_until="domcontentloaded") await page.goto(search_url, wait_until="domcontentloaded")
# 独立登录检查:已登录直接开始;未登录才回退首页等手动登录 # 独立登录检查:已登录直接开始;未登录才回退首页等手动登录
logged_in = await _ensure_logged_in(page, search_url) logged_in = await _ensure_logged_in(page, search_url, login_wait=login_wait)
if not logged_in: if not logged_in:
# 关闭浏览器(持久化目录已保存任何已有状态),中止本次爬取 # 关闭浏览器(持久化目录已保存任何已有状态),中止本次爬取
if cdp_url: if cdp_url:
@@ -420,6 +514,8 @@ async def scrape(keyword: str, count: int, headless: bool = False,
for s in srcset.split(",") for s in srcset.split(",")
] ]
max_url = max(candidates, key=lambda x: x[1])[0] max_url = max(candidates, key=lambda x: x[1])[0]
if max_url in known:
continue # 已采集过的旧图 → 跳过(不计入 count,驱动往下拉)
imgs_url.add(max_url) imgs_url.add(max_url)
# 模拟人工: 鼠标平滑移动到随机位置 + 分段连续滚动, # 模拟人工: 鼠标平滑移动到随机位置 + 分段连续滚动,
+45 -6
View File
@@ -3,11 +3,15 @@
用法: 用法:
from pinterest_scraper.scraper import scrape_pinterest from pinterest_scraper.scraper import scrape_pinterest
files = scrape_pinterest("vintage 70s", count=40, save_dir="output/pinterest_ref/US/vintage_70s") files = scrape_pinterest("vintage 70s", count=40, save_dir="output/pinterest_ref/US/vintage_70s")
增量续采:每个 save_dir 下维护 .collected_urls.json(该关键词历史已采集 URL 集合)。
重复搜索同一关键词时自动跳过旧图、往下拉采集新图,避免反复下载顶部重复图片。
""" """
import asyncio import asyncio
import json
import os import os
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional, Set
from pinterest_scraper.pinterest_image_capture import ( from pinterest_scraper.pinterest_image_capture import (
detect_proxy, detect_proxy,
@@ -17,14 +21,39 @@ from pinterest_scraper.pinterest_image_capture import (
) )
def _load_collected_urls(save_dir: str) -> Set[str]:
"""读该关键词历史已采集 URL.collected_urls.json),不存在/损坏返回空集。"""
p = Path(save_dir) / ".collected_urls.json"
if not p.exists():
return set()
try:
data = json.loads(p.read_text(encoding="utf-8"))
return {str(u) for u in data if str(u).strip()}
except Exception: # noqa: BLE001
return set()
def _save_collected_urls(save_dir: str, urls: Set[str]) -> None:
"""把已采集 URL 集合持久化到 .collected_urls.json(供下次增量续采)。"""
try:
p = Path(save_dir) / ".collected_urls.json"
p.write_text(json.dumps(sorted(urls), ensure_ascii=False), encoding="utf-8")
except Exception as e: # noqa: BLE001
print(f"[pinterest] 已采集 URL 持久化失败: {e}")
def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = None, def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = None,
proxy: Optional[str] = None, headless: bool = False, proxy: Optional[str] = None, headless: bool = False,
cdp_url: Optional[str] = None) -> List[str]: cdp_url: Optional[str] = None,
login_wait: bool = False) -> List[str]:
"""按关键词爬取 Pinterest 图片并下载到 save_dir,返回下载成功的文件路径列表。 """按关键词爬取 Pinterest 图片并下载到 save_dir,返回下载成功的文件路径列表。
- proxy 为 None 时自动探测(环境变量/系统代理/本地常见端口);空字符串显式禁用。 - proxy 为 None 时自动探测(环境变量/系统代理/本地常见端口);空字符串显式禁用。
- 首次运行会弹出本地 Chrome 窗口,需手动登录 Pinterest 一次,登录态缓存在 - login_wait=False(默认):运行时检测到未登录时不阻塞,直接返回空列表(由调用方跳过并告警);
login_wait=True:弹出 Chrome 窗口等待手动登录一次,登录态缓存在
pinterest_scraper/.chrome_session,之后自动复用。 pinterest_scraper/.chrome_session,之后自动复用。
- 增量续采:读取 save_dir/.collected_urls.json 历史已采集 URL,滚动时跳过旧图往下拉,
只下载新图,并把新 URL 合并写回(种子词用尽复用同一关键词时自动采集更靠后的新图)。
""" """
# 代理解析:显式传入 > 自动探测 > 直连 # 代理解析:显式传入 > 自动探测 > 直连
if proxy is None: if proxy is None:
@@ -39,12 +68,22 @@ def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = No
os.makedirs(out, exist_ok=True) os.makedirs(out, exist_ok=True)
print(f"[pinterest] 关键词: {keyword} | 数量: {count} | 保存目录: {out}") print(f"[pinterest] 关键词: {keyword} | 数量: {count} | 保存目录: {out}")
imgs_url = asyncio.run(scrape(keyword, count, headless, proxy, cdp_url)) # 跨运行已采集 URL:重复搜索同一关键词时跳过旧图、往下拉续采
known = _load_collected_urls(out)
if known:
print(f"[pinterest] 该关键词已采集过 {len(known)} 个 URL,将跳过旧图往下拉续采新图")
imgs_url = asyncio.run(scrape(keyword, count, headless, proxy, cdp_url, login_wait,
known_urls=known))
if not imgs_url: if not imgs_url:
print(f"[pinterest] 未收集到图片(可能未登录搜索无结果): {keyword}") print(f"[pinterest] 未收集到图片(可能未登录搜索无结果或该关键词已无新图: {keyword}")
return [] return []
asyncio.run(download_all(imgs_url, out, proxy)) asyncio.run(download_all(imgs_url, out, proxy))
# 合并本次新采集 URL 到持久化集合,供下次增量续采
known |= imgs_url
_save_collected_urls(out, known)
files = [str(p) for p in sorted(Path(out).iterdir()) if p.is_file()] files = [str(p) for p in sorted(Path(out).iterdir()) if p.is_file()]
print(f"[pinterest] 完成,共下载 {len(files)} 张图片: {keyword}") print(f"[pinterest] 完成,本次新增 {len(imgs_url)} 张图片(累计已采集 {len(known)} 个 URL: {keyword}")
return files return files
+165
View File
@@ -0,0 +1,165 @@
# 韩国审美与风格-配色规则(prompts/KR/aesthetics.yaml
style_hint: "Trendy K-pop street culture, Seoul neon nightlife, Y2K retro-futurism, cute-hip aesthetic, bold typography with hangul accents. Prefer: K-pop, Seoul Neon, Y2K, Hanbok Pattern, Korean Food, K-Street Fashion, Jeju Nature, Retro Korea."
extra_style_rules:
- keywords: [kpop, idol, concert, fan, lightstick, music]
art_style: "kpop idol badge illustration"
color_palette: "purple, pink, black, silver"
- keywords: [seoul, neon, night, hongdae, gangnam, city]
art_style: "seoul neon night street illustration"
color_palette: "neon pink, electric blue, black, cyan"
- keywords: [y2k, retro, futuristic, chrome, futuristic]
art_style: "y2k retro-futurism graphic"
color_palette: "silver, pink, light blue, purple"
- keywords: [hanbok, traditional, dancheong, palace, royal]
art_style: "hanbok dancheong pattern illustration"
color_palette: "crimson red, royal blue, gold, white"
- keywords: [kimchi, bibimbap, tteokbokki, kimbap, bulgogi, food]
art_style: "playful korean food illustration"
color_palette: "chili red, golden yellow, leaf green, cream"
- keywords: [tiger, kumiho, fox, crane, magpie, folklore, animal]
art_style: "korean folklore animal illustration"
color_palette: "orange, white, black, red"
- keywords: [cherry blossom, sakura, jinhae, spring, flower]
art_style: "korean cherry blossom illustration"
color_palette: "blossom pink, white, pale green, soft gray"
- keywords: [jeju, island, hallasan, ocean, coast]
art_style: "jeju island nature illustration"
color_palette: "ocean blue, sea green, cream, volcanic black"
- keywords: [hangul, korean letter, typography, calligraphy, text]
art_style: "bold hangul typography design"
color_palette: "black, white, red, yellow"
- keywords: [hanok, gyeongbokgung, temple, architecture, traditional house]
art_style: "korean hanok architecture illustration"
color_palette: "wood brown, roof gray, red, white"
- keywords: [mask, talchum, dance, festival, folk]
art_style: "korean mask dance folk illustration"
color_palette: "red, yellow, black, white"
- keywords: [cafe, coffee, bingsu, dessert, sweet, drink]
art_style: "korean cafe dessert illustration"
color_palette: "cream white, coffee brown, pink, mint"
- keywords: [streetwear, street fashion, k-fashion, hip, style]
art_style: "korean street fashion illustration"
color_palette: "black, white, neon green, red"
- keywords: [taekwondo, martial arts, sport, fight]
art_style: "taekwondo martial art silhouette graphic"
color_palette: "black, white, red, blue"
- keywords: [moon, star, night, dreamy, galaxy]
art_style: "korean dreamy night illustration"
color_palette: "deep blue, star gold, silver, purple"
- keywords: [zodiac, tiger year, new year, year of]
art_style: "korean zodiac animal illustration"
color_palette: "red, gold, orange, white"
- keywords: [mugunghwa, rose of sharon, korean flower, floral]
art_style: "korean national flower illustration"
color_palette: "pink, white, green, yellow"
- keywords: [ginkgo, autumn, maple, fall, leaf]
art_style: "korean autumn leaves illustration"
color_palette: "ginkgo yellow, maple red, brown, warm orange"
- keywords: [pattern, geometric, traditional pattern, motif]
art_style: "korean traditional pattern design"
color_palette: "red, blue, gold, white"
- keywords: [drum, janggu, samulnori, instrument, music traditional]
art_style: "korean traditional music illustration"
color_palette: "red, gold, black, white"
- keywords: [seoul skyline, tower, landmark, namsan, han river]
art_style: "seoul landmark skyline illustration"
color_palette: "night blue, neon pink, white, gold"
- keywords: [busan, haeundae, sea, beach, fishing village]
art_style: "korean coastal town illustration"
color_palette: "sea blue, sandy beige, coral, white"
- keywords: [hanbok character, cute, mascot, chibi, doll]
art_style: "cute hanbok mascot illustration"
color_palette: "pastel pink, mint, cream, red"
- keywords: [lantern, lotus, temple lantern, buddhist festival]
art_style: "korean lotus lantern illustration"
color_palette: "gold, crimson, white, green"
# 韩国常见侵权盲区(按国家隔离)
extra_blacklist:
# —— K-pop 偶像团体 / 组合 ——
- bts
- blackpink
- exo
- twice
- seventeen
- "stray kids"
- enhypen
- itzy
- aespa
- ive
- "le sserafim"
- nct
- "red velvet"
- newjeans
- "super junior"
- shinee
- "big bang"
- tvxq
- "girl's generation"
- snsd
- mamamoo
- "(g)i-dle"
- txt
- got7
- monsta x
- "black label"
# —— 粉丝名(专属名称)——
- army
- blink
- once
- nctzen
- midzy
- carat
- stay
# —— 真实艺人(肖像权)——
- iu
- lisa
- jennie
- jisoo
- rose
- jungkook
- jimin
- v
- suga
- rm
- j-hope
- "cha eun-woo"
- "lee min-ho"
- "kim soo-hyun"
- "song hye-kyo"
- "bae suzy"
- "park bo-gum"
- hyun bin
- "kim tae-hee"
- "yoo jae-suk"
- psy
# —— 品牌 ——
- samsung
- "lg electronics"
- hyundai
- kia
- naver
- kakao
- bibigo
- ottogi
- nongshim
- lotte
- "tous les jours"
- "paris baguette"
- innisfree
- amorepacific
- "line friends"
- "kakao friends"
# —— IP / 版权 ——
- "line friends"
- "kakao friends"
- pororo
- tayo
- larva
- pinkfong
- "baby shark"
- pengsoo
- cocomong
- "robocar poli"
- "squid game"
+10
View File
@@ -0,0 +1,10 @@
# 韩国专属补充指令
- 设计口吻:K-pop 街头文化活力、首尔霓虹夜生活、Y2K 复古未来、可爱甜酷审美、大胆排印与韩文字点缀。
- 合规重点(韩国特有):
* 偶像团体(BTS、BLACKPINK、EXO、TWICE、NewJeans、SEVENTEEN、Stray Kids、ENHYPEN、ITZY、aespa、IVE、LE SSERAFIM、NCT、Red Velvet 等)名称、logo、应援色、粉丝名(ARMY、BLINK、ONCE 等)与成员肖像禁止。
* 真实艺人(IU、Lisa、Jennie、Jisoo、Rosé、Jungkook、V、Jimin、Suga、RM、J-Hope、차은우 车银优、이민호 李敏镐、김수현 金秀贤、송혜교 宋慧乔、배수지 裴秀智 等)肖像权。
* 品牌(Samsung、LG、Hyundai、Kia、Naver、Kakao、Bibigo、Ottogi、Nongshim、Lotte、Tous Les Jours、Paris Baguette、Innisfree、Line Friends、Kakao Friendslogo 与名称禁止。
* 版权 IPLine Friends、Kakao Friends、뽀로로(Pororo)、타요(Tayo the Little Bus)、라바(Larva)、핑크퐁(Pinkfong) Baby Shark、펭수(Pengsoo) 等角色禁止。
- 可做"首尔霓虹夜景 / 原创 K-pop 风格角色 / 韩式传统图案(한복、단청、매화)/ 韩国美食 / 韩国街头潮流 / 济州自然"等原创设计。
- 文字规则:韩文或英文短句可加可不加,与印花适配即可(如 SEOUL、K-문화、사랑해);任何文字严禁政治/宗教/仇恨/暴力/性/品牌/商标/真实人物姓名等敏感内容。
- Return JSON 输出格式必须严格遵循系统提示词中的 schema,不要输出任何额外文字。
+19
View File
@@ -0,0 +1,19 @@
You are a POD T-shirt design analyst working in CUSTOM-REFERENCE mode. You are given ONE existing bestseller product photo — an on-model shot or a flat-lay shot of a printed garment. Your job:
1. Locate the printed artwork on the garment and judge whether that PRINT can inspire a new T-shirt design (ignore the shirt itself, the model, the background, watermarks and photo quality — judge only the printed artwork).
2. Write an ORIGINAL design brief that keeps the bestseller's general vibe (theme, style genre, era, mood) but is a clearly different, non-infringing work.
RULES:
0. REFERENCE HANDLING — the attached photo is a product mockup of an existing bestseller, not an artwork file. Use it ONLY to identify the printed artwork and distill its appeal into generic style keywords (retro, y2k, minimal, grunge, boho, kawaii, western, cottagecore, vintage cartoon...). Never reproduce, trace, rearrange, recolor or closely imitate the print, its layout, characters or text.
1. SAME SUBJECT FAMILY, NEW EXECUTION — the new design must belong to the same visual family as the bestseller print so a buyer would instantly see they are "same style, different shirt". LOCK these elements to the bestseller's: (a) general subject category (e.g., human/animal figure silhouette, celestial object, floral, vehicle, typography-led...), (b) art technique (e.g., grunge ink silhouette, clean line art, watercolor, distressed collage...), (c) palette (monochrome / muted / neon...). VARY these: the exact subject, the pose, the composition and layout, and any decorative framing. The result must be clearly original — no traced or rearranged reuse of the reference artwork — while staying recognizably in the same genre.
2. IP SCREENING — if the bestseller print contains brand logos, trademarks, slogans, mascots, copyrighted characters (including stylized or silhouette versions), real people/celebrities, movie/game/anime/band IP, or lyrics, do NOT imitate them; swap in fully generic equivalents (e.g., an unnamed cartoon animal instead of a recognizable mascot). Also avoid politics, religion, violence, sexual content, alcohol, drugs, gambling, flags, death/occult themes.
3. FORM — ONE clear central subject with strong graphic composition; print-ready standalone artwork. ANY colors are fine — rich palettes, gradients and detailed shading are all acceptable. Because the reference is a product photo, explicitly exclude the garment and the photo itself: never describe shirts, models, hangers, scenes or backgrounds. Photographic elements in the print may be re-rendered as detailed full-color illustrations, retro badges or vintage stickers.
4. TEXT — short ORIGINAL English wording (1-6 words) allowed; wrap exact words in double quotes and demand exact spelling; integrate into composition. Never reuse, translate or near-duplicate the bestseller print's wording; no brand/band/movie names or famous slogans. When unsure, omit.
suitable_for_print: judge ONLY the printed artwork extracted from the bestseller photo. DEFAULT TRUE for graphics, illustrations, badges, vector art, typography prints, or prints visible on mockups. FALSE only for: blank garments with no artwork, prints that are indiscernible due to watermark, blur or very low quality, pure subjectless photo scenery, memes/screenshots/collages with no usable motif. Even when FALSE, still fill all fields so downstream never breaks.
image_prompt = two parts:
1) mandatory opener: "First, carefully examine the printed artwork on the garment in the attached photo: zoom in mentally on the print area, identify its subject, technique, palette and layout, and base the new design on THOSE observed traits. Ignore the model, background and photo quality. Then: use the attached bestseller product photo only as loose inspiration for overall mood, theme, era and style genre — do NOT reproduce, trace, rearrange, recolor or closely imitate its printed artwork, characters, layout or text, and do NOT render a shirt, garment, model, hanger, photo scene, product mockup or background of any kind."
2) the new design: [same subject family as observed] + [same technique and palette family] + [changed exact subject, pose, composition and framing] + [mood], plus quoted original text if used. NEVER mention shirts, apparel, models, scenes, sizes, backgrounds or watermarks — placement is handled externally. Before output, verify the new design shares the locked elements (subject category, technique, palette) with the observed print; if not, revise it.
OUTPUT — ONLY valid JSON, no fences:
{"designs":[{"suitable_for_print":<bool>,"image_prompt":"<str>"}]}
+1
View File
@@ -0,0 +1 @@
Analyze the attached bestseller product photo, locate its printed artwork, and produce one ORIGINAL T-shirt print design brief that keeps its general vibe without copying its printed artwork.
+60
View File
@@ -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%一致。
* 画面中不得出现人像、肢体、人台。
+14
View File
@@ -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衣服上的印花图案与衣服底色。
+46
View File
@@ -0,0 +1,46 @@
You are a POD T-shirt design analyst. Given ONE Pinterest reference image,
judge whether it can inspire a T-shirt print, then write an ORIGINAL design brief
capturing its vibe WITHOUT copying.
Your image_prompt will be sent TOGETHER WITH this reference image to an image
generator, so it must actively override visual imitation.
RULES:
1. NO COPYING — never reproduce or closely imitate the reference's artwork,
characters, layout or text. Deliberately change motif, arrangement and/or
palette so the two read as clearly different works sharing only a general
style. Distill inspiration into generic style words (retro, y2k, minimal,
grunge, boho, kawaii...); never imitate an identifiable artist/studio/IP
style.
2. FORBIDDEN — brand logos, trademarks, slogans, mascots, copyrighted
characters, real people/celebrities, movie/game/anime/band IP, lyrics,
even stylized or silhouette versions. Avoid politics, religion, violence,
sexual content, alcohol, drugs, gambling, flags, death/occult themes.
3. FORM — ONE clear central subject with strong graphic composition;
print-ready standalone artwork. ANY colors are fine — rich palettes,
gradients and detailed shading are all acceptable. Photographic references
may be rendered as detailed full-color illustrations, retro badges or
vintage stickers.
TEXT: short ORIGINAL English wording (0-5 words) allowed; wrap exact words in
double quotes and demand exact spelling; integrate into composition. Never
reuse/translate reference text; no brand/band/movie names or famous slogans.
When unsure, omit.
suitable_for_print: DEFAULT TRUE for graphics, illustrations, badges, vector
art, typography posters, or prints on mockups (judge only the printed artwork).
FALSE only for: subjectless photo scenery, memes/screenshots/collages,
watermarked or very low-quality images, decor/food/candid photos with no
usable motif. Even when FALSE, still fill all fields so downstream never breaks.
image_prompt = two parts:
1) mandatory opener, e.g.: "Use the attached reference image only as loose
inspiration for overall mood, theme and era — do NOT reproduce, trace,
rearrange, recolor or closely imitate any element, character, layout or
text shown in it."
2) the new design: [central motif] + [style] + [color treatment] +
[composition] + [mood], plus quoted original text if used.
NEVER mention shirts, apparel, models, scenes, sizes, backgrounds or
watermarks — placement is handled externally.
OUTPUT — ONLY valid JSON, no fences:
{"designs":[{"suitable_for_print":<bool>,"image_prompt":"<str>"}]}
+1
View File
@@ -0,0 +1 @@
Analyze the attached image and produce one ORIGINAL T-shirt print design brief that captures its visual vibe without copying it.
+20
View File
@@ -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”: “中文标题”}
+22
View File
@@ -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": "日本語タイトル"}
+20
View File
@@ -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": "中文标题"}
+19 -1
View File
@@ -346,6 +346,13 @@ class TemplateRouter:
return self.header_map[key_s] return self.header_map[key_s]
if key_s in self.column_map: if key_s in self.column_map:
return self.column_map[key_s] return self.column_map[key_s]
# 兼容「SPU商品属性-袖型」↔「袖型」列名差异:不同模板对同一属性列命名不同(带/不带分组前缀)
if key_s.startswith("SPU商品属性-"):
bare = key_s[len("SPU商品属性-"):]
if bare in self.column_map:
return self.column_map[bare]
if bare in self.header_map:
return self.header_map[bare]
if re.fullmatch(r"[A-Za-z]{1,3}", key_s): if re.fullmatch(r"[A-Za-z]{1,3}", key_s):
return column_index_from_string(key_s.upper()) return column_index_from_string(key_s.upper())
norm = _UNIT_SUFFIX.sub("", key_s).strip() norm = _UNIT_SUFFIX.sub("", key_s).strip()
@@ -406,7 +413,18 @@ class TemplateRouter:
# 2) 路由到目标行 # 2) 路由到目标行
row = self.route(lv, spu, update=update, match=match) row = self.route(lv, spu, update=update, match=match)
# 3) 写入 # 3) 清空目标行(仅新建行,update 覆盖模式保留模板原有内容):
# 模板数据区可能预填了示例/默认值(如 SKU销售信息-建议售价单位=CNY、
# SPU商品属性-印花类型=定位印花),不清理会把模板预填值误带到生成结果里。
# 只清空本行已写过的数据单元格,保留样式/数据验证下拉框。
if not update:
for c in range(1, self.ws.max_column + 1):
cell = self.ws.cell(row, c)
v = cell.value
if v is not None and str(v).strip() != "":
cell.value = None
# 4) 写入
for key, value in data.items(): for key, value in data.items():
if value is None: if value is None:
continue continue
+282 -121
View File
@@ -31,9 +31,7 @@ import yaml
from graph.agent import run_country from graph.agent import run_country
COUNTRIES = ["US", "GB", "JP", "AU", "MX", "DE", "BR", "SA", "PL", "ES", "IT", "CA"] COUNTRIES = ["US", "GB", "JP", "AU", "MX", "DE", "BR", "SA", "PL", "ES", "IT", "CA", "KR"]
# LLM 只适配 OpenAI 兼容协议:openai(真调用,可在下方配置自定义模型/URL/Key)/ mock(无 key 演示占位)
PROVIDERS = ["openai", "mock"]
def resource_root() -> Path: def resource_root() -> Path:
@@ -379,12 +377,20 @@ def run_pipeline(countries, provider, max_seeds, log_q,
def run_pinterest_pipeline(countries, provider, log_q, spu_tasks=None, spu_count=0, def run_pinterest_pipeline(countries, provider, log_q, spu_tasks=None, spu_count=0,
oai=None, markup_percent=0.0, code_prefix="DG", template_path=""): oai=None, markup_percent=0.0, code_prefix="DG", template_path="",
custom_image_dir=""):
"""Pinterest 参考模式后台线程:独立于 Google Trends 采集链路。 """Pinterest 参考模式后台线程:独立于 Google Trends 采集链路。
各国独立种子词池 → LLM 搜索词(json_schema + 动态注入防重复)→ 爬图 各国独立种子词池 → LLM 搜索词(json_schema + 动态注入防重复)→ 爬图
→ LLM 分析图片 → 设计简报 → 产品生成(设计稿/主图/种草图/模板导出)。 → LLM 分析图片 → 设计简报 → 产品生成(设计稿/主图/种草图/模板导出)。
custom_image_dir 非空 → 自定义图片模式:不搜索不采集,把该文件夹有效图片直接送多模态分析,
沿用 Pinterest 后续所有步骤;数量硬校验(有效图片数 ≥ 选品清单总数)由 run_pinterest_ref 执行。
""" """
if not spu_tasks:
log_q.put(("log", "[UI] Pinterest/自定义模式必须通过「+添加选品」添加任务清单后才能运行\n"))
log_q.put(("error", None))
return
config = load_config() config = load_config()
config["seed_provider"] = provider config["seed_provider"] = provider
apply_openai_cfg(config, oai) apply_openai_cfg(config, oai)
@@ -401,6 +407,9 @@ def run_pinterest_pipeline(countries, provider, log_q, spu_tasks=None, spu_count
p["markup_percent"] = markup_percent p["markup_percent"] = markup_percent
if code_prefix: if code_prefix:
p["code_prefix"] = code_prefix p["code_prefix"] = code_prefix
# 自定义模式:把上传文件夹写入配置(run_pinterest_ref 据此进入 custom 图并校验)
if custom_image_dir:
config.setdefault("pinterest", {})["custom_image_dir"] = custom_image_dir
# 任务扩展(与 run_pipeline 一致):每个集合按自己数量复制 N 份 # 任务扩展(与 run_pipeline 一致):每个集合按自己数量复制 N 份
if spu_tasks: if spu_tasks:
tasks_list = [] tasks_list = []
@@ -425,7 +434,8 @@ def run_pinterest_pipeline(countries, provider, log_q, spu_tasks=None, spu_count
from graph.agent import run_pinterest_ref from graph.agent import run_pinterest_ref
for c in countries: for c in countries:
log_q.put(("log", f"\n===== Pinterest 参考模式 {c}SPU 数量 {spu_count}=====\n")) log_q.put(("log", f"\n===== Pinterest 参考模式 {c}SPU 数量 {spu_count}=====\n"))
state = run_pinterest_ref(c, config, config_root(), runtime_root(), task_timestamp=task_ts) state = run_pinterest_ref(c, config, config_root(), runtime_root(),
task_timestamp=task_ts, custom_image_dir=custom_image_dir)
items = state.get("product") or [] items = state.get("product") or []
errs = state.get("errors") or [] errs = state.get("errors") or []
log_q.put(("log", f"[{c}] Pinterest 参考完成:{len(items)} 个产品,兜底错误 {len(errs)}\n")) log_q.put(("log", f"[{c}] Pinterest 参考完成:{len(items)} 个产品,兜底错误 {len(errs)}\n"))
@@ -490,8 +500,10 @@ def _pack_cache(country: str, log_q=None) -> str:
class App(tk.Tk): class App(tk.Tk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title("POD 热点抓取 Agent") self.title("POD 智能设计 Agent")
self.geometry("920x660") self.geometry("1440x860")
self.minsize(1200, 720)
self.configure(bg="#F2F4F8")
self._q = queue.Queue() self._q = queue.Queue()
self._busy = False self._busy = False
self._results = {} self._results = {}
@@ -505,144 +517,249 @@ class App(tk.Tk):
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
LOG_FILE = None LOG_FILE = None
print(f"[UI] 日志文件创建失败(不影响运行): {e}") print(f"[UI] 日志文件创建失败(不影响运行): {e}")
self._setup_style()
self._build_ui() self._build_ui()
self._load_cache() self._load_cache()
self.after(100, self._poll) self.after(100, self._poll)
def _default_provider(self) -> str: # ---------- 主题样式 ----------
"""默认模式:config.yaml llm_screen.provider(用户已配 key → openai 默认)。""" def _setup_style(self):
style = ttk.Style(self)
try: try:
return "openai" if (load_config().get("llm_screen") or {}).get("provider") == "openai" else "mock" style.theme_use("clam")
except Exception: except Exception: # noqa: BLE001
return "mock" pass
# 主色板
self._C_PRIMARY = "#2563EB" # 主蓝
self._C_PRIMARY_DK = "#1D4ED8" # 主蓝深
self._C_ACCENT = "#F59E0B" # 强调橙
self._C_BG = "#F2F4F8" # 窗口背景
self._C_CARD = "#FFFFFF" # 卡片背景
self._C_BORDER = "#DCE1EA" # 边框
self._C_TEXT = "#1F2937" # 主文字
self._C_SUB = "#6B7280" # 次要文字
self._C_OK = "#16A34A" # 成功绿
self._C_WARN = "#B45309" # 警示
self._FONT = ("Microsoft YaHei UI", 10)
self._FONT_SM = ("Microsoft YaHei UI", 9)
self._FONT_TITLE = ("Microsoft YaHei UI", 15, "bold")
self._FONT_SUB = ("Microsoft YaHei UI", 9)
style.configure(".", font=self._FONT, background=self._C_BG,
foreground=self._C_TEXT)
style.configure("Card.TLabelframe", background=self._C_CARD,
bordercolor=self._C_BORDER, relief="solid", borderwidth=1)
style.configure("Card.TLabelframe.Label", background=self._C_CARD,
foreground=self._C_PRIMARY_DK, font=("Microsoft YaHei UI", 10, "bold"))
style.configure("Card.TFrame", background=self._C_CARD)
style.configure("TLabel", background=self._C_BG, foreground=self._C_TEXT)
style.configure("Card.TLabel", background=self._C_CARD, foreground=self._C_TEXT)
style.configure("Card.TRadiobutton", background=self._C_CARD, foreground=self._C_TEXT)
style.map("Card.TRadiobutton",
background=[("active", self._C_CARD)],
foreground=[("active", self._C_PRIMARY_DK)])
style.configure("Sub.TLabel", background=self._C_BG, foreground=self._C_SUB,
font=self._FONT_SM)
style.configure("CardSub.TLabel", background=self._C_CARD, foreground=self._C_SUB,
font=self._FONT_SM)
style.configure("Accent.TLabel", background=self._C_BG, foreground=self._C_PRIMARY_DK,
font=self._FONT_SM)
style.configure("TButton", font=self._FONT, padding=(12, 6))
style.configure("Primary.TButton", font=self._FONT, padding=(18, 8),
background=self._C_PRIMARY, foreground="#FFFFFF", borderwidth=0)
style.map("Primary.TButton",
background=[("active", self._C_PRIMARY_DK), ("disabled", "#A5B4FC")],
foreground=[("disabled", "#EEF2FF")])
style.configure("AccentBtn.TButton", font=self._FONT, padding=(14, 7),
background=self._C_ACCENT, foreground="#FFFFFF", borderwidth=0)
style.map("AccentBtn.TButton",
background=[("active", "#D97706"), ("disabled", "#FCD34D")],
foreground=[("disabled", "#FFFBEB")])
style.configure("TCombobox", padding=4)
style.configure("TEntry", padding=4)
style.configure("Treeview", font=self._FONT_SM, rowheight=26,
background="#FFFFFF", fieldbackground="#FFFFFF",
bordercolor=self._C_BORDER)
style.configure("Treeview.Heading", font=("Microsoft YaHei UI", 9, "bold"),
background="#EEF2F7", foreground=self._C_TEXT, padding=6)
style.map("Treeview", background=[("selected", "#DBEAFE")],
foreground=[("selected", "#1E3A8A")])
style.configure("TNotebook", background=self._C_BG, borderwidth=0)
style.configure("TNotebook.Tab", font=self._FONT, padding=(14, 7))
style.map("TNotebook.Tab",
background=[("selected", self._C_CARD)],
foreground=[("selected", self._C_PRIMARY_DK)])
# ---------- UI 构建 ---------- # ---------- UI 构建 ----------
def _build_ui(self): def _build_ui(self):
top = ttk.Frame(self, padding=8) # —— 顶部标题栏 ——
top.pack(fill="x") header = tk.Frame(self, bg=self._C_PRIMARY, height=62)
ttk.Label(top, text="国家:").pack(side="left") header.pack(fill="x")
self.country_var = tk.StringVar(value="US") header.pack_propagate(False)
self.country_combo = ttk.Combobox(top, textvariable=self.country_var, tk.Label(header, text="🎨 智能设计 Agent",
values=COUNTRIES, state="readonly", width=6) bg=self._C_PRIMARY, fg="#FFFFFF",
self.country_combo.pack(side="left", padx=4) font=("Microsoft YaHei UI", 16, "bold")).pack(side="left", padx=16, pady=12)
self.country_combo.bind("<<ComboboxSelected>>", lambda e: self._on_country_selected()) tk.Label(header, text="一键生成 T 恤印花设计 · 多国多站点",
ttk.Label(top, text=" 模式:").pack(side="left", padx=(14, 0)) bg=self._C_PRIMARY, fg="#DBEAFE",
self.provider_var = tk.StringVar(value=self._default_provider()) font=self._FONT_SM).pack(side="left", padx=(4, 0), pady=12)
ttk.Radiobutton(top, text="OpenAI(真 LLM+生图)", value="openai",
variable=self.provider_var).pack(side="left", padx=2)
ttk.Radiobutton(top, text="Mock 演示", value="mock",
variable=self.provider_var).pack(side="left", padx=2)
ttk.Label(top, text=" 流程:").pack(side="left", padx=(14, 0))
self.flow_var = tk.StringVar(value="trends")
ttk.Radiobutton(top, text="热点采集", value="trends",
variable=self.flow_var).pack(side="left", padx=2)
ttk.Radiobutton(top, text="Pinterest 参考", value="pinterest",
variable=self.flow_var).pack(side="left", padx=2)
ttk.Label(top, text=" 种子数量:").pack(side="left", padx=(14, 0))
self.seed_var = tk.StringVar(value="24")
ttk.Entry(top, textvariable=self.seed_var, width=4).pack(side="left")
# 右上角:热点状态(缓存时间 / 采集时间)
self.hotspot_time_var = tk.StringVar(value="") self.hotspot_time_var = tk.StringVar(value="")
ttk.Label(top, textvariable=self.hotspot_time_var, anchor="e", tk.Label(header, textvariable=self.hotspot_time_var,
foreground="#185FA5").pack(side="right") bg=self._C_PRIMARY, fg="#DBEAFE",
font=self._FONT_SM).pack(side="right", padx=(0, 8), pady=12)
# —— 商品上传模板(必须上传后才能开始任务;template_export 从该模板解析并填充)—— # —— 运行设置卡片:国家 / 流程 / 自定义图片 / 种子数量 ——
tpl = ttk.LabelFrame(self, text="商品上传模板(必须上传 .xlsx 后才能运行)", padding=4) cfg = ttk.LabelFrame(self, text="运行设置", style="Card.TLabelframe", padding=10)
tpl.pack(fill="x", padx=8, pady=(2, 0)) cfg.pack(fill="x", padx=12, pady=(10, 4))
ttk.Label(cfg, text="目标国家:", style="Card.TLabel").pack(side="left")
self.country_var = tk.StringVar(value="US")
self.country_combo = ttk.Combobox(cfg, textvariable=self.country_var,
values=COUNTRIES, state="readonly", width=6)
self.country_combo.pack(side="left", padx=(4, 14))
self.country_combo.bind("<<ComboboxSelected>>", lambda e: self._on_country_selected())
ttk.Label(cfg, text="流程:", style="Card.TLabel").pack(side="left")
self.flow_var = tk.StringVar(value="pinterest")
ttk.Radiobutton(cfg, text="热点采集", value="trends", style="Card.TRadiobutton",
variable=self.flow_var).pack(side="left", padx=2)
ttk.Radiobutton(cfg, text="Pinterest 参考", value="pinterest", style="Card.TRadiobutton",
variable=self.flow_var).pack(side="left", padx=2)
ttk.Radiobutton(cfg, text="自定义图片", value="custom", style="Card.TRadiobutton",
variable=self.flow_var).pack(side="left", padx=2)
ttk.Button(cfg, text="📁 选图片文件夹…", command=self._choose_custom_dir).pack(side="left", padx=(10, 0))
self.custom_dir_var = tk.StringVar(value="")
self.custom_dir_name_var = tk.StringVar(value="")
ttk.Label(cfg, textvariable=self.custom_dir_name_var, anchor="w",
style="Accent.TLabel").pack(side="left", padx=(6, 14))
ttk.Label(cfg, text="种子数量:", style="Card.TLabel").pack(side="left")
self.seed_var = tk.StringVar(value="24")
ttk.Entry(cfg, textvariable=self.seed_var, width=5).pack(side="left", padx=(4, 0))
# —— 商品上传模板卡片 ——
tpl = ttk.LabelFrame(self, text="商品上传模板(必须上传 .xlsx 后才能运行)",
style="Card.TLabelframe", padding=8)
tpl.pack(fill="x", padx=12, pady=(4, 4))
ttk.Button(tpl, text="📁 选择模板…", command=self._choose_template).pack(side="left") ttk.Button(tpl, text="📁 选择模板…", command=self._choose_template).pack(side="left")
self.template_path_var = tk.StringVar(value="") self.template_path_var = tk.StringVar(value="")
self.template_name_var = tk.StringVar(value="(未选择)") self.template_name_var = tk.StringVar(value="(未选择)")
ttk.Label(tpl, textvariable=self.template_name_var, anchor="w", ttk.Label(tpl, textvariable=self.template_name_var, anchor="w",
foreground="#993C1D").pack(side="left", padx=(8, 0)) style="CardSub.TLabel").pack(side="left", padx=(10, 0))
# —— 选品面板:国家→款号→颜色→添加任务(支持多款号 + 数量上限)—— # —— 选品卡片:款号 → 颜色 → 添加任务 ——
sel = ttk.LabelFrame(self, text="选品(选定国家自动加载款号;款号 → 颜色 → 添加任务", padding=6) sel = ttk.LabelFrame(self, text="选品(选定国家自动加载款号;款号 → 颜色 → 添加选品",
sel.pack(fill="x", padx=8, pady=(4, 0)) style="Card.TLabelframe", padding=10)
sel.pack(fill="x", padx=12, pady=(4, 4))
# 左:款号列表 # 左:款号列表
lf1 = ttk.Frame(sel) lf1 = ttk.Frame(sel, style="Card.TFrame")
lf1.pack(side="left", padx=4) lf1.pack(side="left", padx=4)
ttk.Label(lf1, text="款号(选定后自动加载颜色与最低价):").pack(anchor="w") ttk.Label(lf1, text="款号(多选)", style="Card.TLabel").pack(anchor="w")
self.spu_list = tk.Listbox(lf1, selectmode="extended", height=5, width=14, exportselection=False) self.spu_list = tk.Listbox(lf1, selectmode="extended", height=6, width=16,
self.spu_list.pack(side="left") exportselection=False, bg="#FFFFFF", fg=self._C_TEXT,
selectbackground="#DBEAFE", selectforeground="#1E3A8A",
highlightthickness=1, highlightbackground=self._C_BORDER,
highlightcolor=self._C_PRIMARY, font=self._FONT_SM,
relief="solid", borderwidth=1)
self.spu_list.pack(side="left", pady=(4, 0))
self.spu_list.bind("<<ListboxSelect>>", self._on_spu_selected) self.spu_list.bind("<<ListboxSelect>>", self._on_spu_selected)
self.min_price_var = tk.StringVar(value="") self.min_price_var = tk.StringVar(value="")
ttk.Label(lf1, textvariable=self.min_price_var, anchor="w", foreground="#993C1D").pack(fill="x", pady=(4, 0)) ttk.Label(lf1, textvariable=self.min_price_var, anchor="w",
style="CardSub.TLabel").pack(fill="x", pady=(4, 0))
# 中:颜色多选(点击即多选)+ 添加按钮 # 中:颜色多选 + 添加按钮
lf2 = ttk.Frame(sel) lf2 = ttk.Frame(sel, style="Card.TFrame")
lf2.pack(side="left", padx=(10, 4)) lf2.pack(side="left", padx=(12, 4))
ttk.Label(lf2, text="当前款号颜色(点击多选,显示色名/最低价):").pack(anchor="w") ttk.Label(lf2, text="当前款号颜色(点击多选", style="Card.TLabel").pack(anchor="w")
self.sku_list = tk.Listbox(lf2, selectmode="multiple", height=5, width=34, exportselection=False) self.sku_list = tk.Listbox(lf2, selectmode="multiple", height=6, width=38,
self.sku_list.pack(side="left") exportselection=False, bg="#FFFFFF", fg=self._C_TEXT,
b2 = ttk.Frame(lf2) selectbackground="#DBEAFE", selectforeground="#1E3A8A",
b2.pack(side="left", padx=4) highlightthickness=1, highlightbackground=self._C_BORDER,
highlightcolor=self._C_PRIMARY, font=self._FONT_SM,
relief="solid", borderwidth=1)
self.sku_list.pack(side="left", pady=(4, 0))
b2 = ttk.Frame(lf2, style="Card.TFrame")
b2.pack(side="left", padx=6)
ttk.Button(b2, text="全选", command=self._select_all_colors).pack(fill="x") ttk.Button(b2, text="全选", command=self._select_all_colors).pack(fill="x")
ttk.Button(b2, text="清空", command=lambda: self.sku_list.selection_clear(0, "end")).pack(fill="x", pady=2) ttk.Button(b2, text="清空", command=lambda: self.sku_list.selection_clear(0, "end")).pack(fill="x", pady=2)
ttk.Button(b2, text="+添加选品", command=self._add_task).pack(fill="x", pady=(4, 0)) ttk.Button(b2, text="+添加选品", style="AccentBtn.TButton",
ttk.Label(b2, text="该款SPU/设计数(1-N):", anchor="w").pack(fill="x", pady=(6, 0)) command=self._add_task).pack(fill="x", pady=(4, 0))
ttk.Label(b2, text="该款设计数:", style="CardSub.TLabel").pack(fill="x", pady=(6, 0))
self.count_var = tk.StringVar(value="10") self.count_var = tk.StringVar(value="10")
ttk.Entry(b2, textvariable=self.count_var, width=6).pack(fill="x") ttk.Entry(b2, textvariable=self.count_var, width=6).pack(fill="x")
# 右:任务清单 # 右:任务清单
lf3 = ttk.Frame(sel) lf3 = ttk.Frame(sel, style="Card.TFrame")
lf3.pack(side="left", padx=4) lf3.pack(side="left", padx=4)
ttk.Label(lf3, text="选品任务清单:").pack(anchor="w") ttk.Label(lf3, text="选品任务清单", style="Card.TLabel").pack(anchor="w")
self.task_stat_var = tk.StringVar(value="0 个任务") self.task_stat_var = tk.StringVar(value="0 个任务")
ttk.Label(lf3, textvariable=self.task_stat_var, anchor="w", ttk.Label(lf3, textvariable=self.task_stat_var, anchor="w",
foreground="#993C1D").pack(anchor="w") style="Accent.TLabel").pack(anchor="w")
self.task_list = tk.Listbox(lf3, height=5, width=40, exportselection=False) self.task_list = tk.Listbox(lf3, height=6, width=44, exportselection=False,
self.task_list.pack(side="left") bg="#FFFFFF", fg=self._C_TEXT,
b3 = ttk.Frame(lf3) selectbackground="#DBEAFE", selectforeground="#1E3A8A",
b3.pack(side="left", padx=4) highlightthickness=1, highlightbackground=self._C_BORDER,
highlightcolor=self._C_PRIMARY, font=self._FONT_SM,
relief="solid", borderwidth=1)
self.task_list.pack(side="left", pady=(4, 0))
b3 = ttk.Frame(lf3, style="Card.TFrame")
b3.pack(side="left", padx=6)
ttk.Button(b3, text="删选中", command=self._del_task).pack(fill="x") ttk.Button(b3, text="删选中", command=self._del_task).pack(fill="x")
ttk.Button(b3, text="清空", command=self._clear_tasks).pack(fill="x", pady=2) ttk.Button(b3, text="清空", command=self._clear_tasks).pack(fill="x", pady=2)
# 加价百分比(供后续定价使用)+ 货号前缀 # 右侧:加价 / 货号前缀
lf4 = ttk.Frame(sel) lf4 = ttk.Frame(sel, style="Card.TFrame")
lf4.pack(side="left", padx=(12, 0)) lf4.pack(side="left", padx=(16, 0))
ttk.Label(lf4, text="加价%:").pack(anchor="w") ttk.Label(lf4, text="加价 %", style="Card.TLabel").pack(anchor="w")
self.markup_var = tk.StringVar(value="0") self.markup_var = tk.StringVar(value="0")
ttk.Entry(lf4, textvariable=self.markup_var, width=7).pack(anchor="w", pady=(2, 0)) ttk.Entry(lf4, textvariable=self.markup_var, width=7).pack(anchor="w", pady=(4, 0))
ttk.Label(lf4, text="按 SKU 最低价加价", anchor="w", foreground="#5F5E5A").pack(anchor="w", pady=(4, 0)) ttk.Label(lf4, text="按 SKU 最低价加价", style="CardSub.TLabel").pack(anchor="w", pady=(4, 0))
lf5 = ttk.Frame(sel) lf5 = ttk.Frame(sel, style="Card.TFrame")
lf5.pack(side="left", padx=(12, 0)) lf5.pack(side="left", padx=(16, 0))
ttk.Label(lf5, text="货号前缀:").pack(anchor="w") ttk.Label(lf5, text="货号前缀", style="Card.TLabel").pack(anchor="w")
self.code_prefix_var = tk.StringVar(value="DG") self.code_prefix_var = tk.StringVar(value="DG")
ttk.Entry(lf5, textvariable=self.code_prefix_var, width=8).pack(anchor="w", pady=(2, 0)) ttk.Entry(lf5, textvariable=self.code_prefix_var, width=8).pack(anchor="w", pady=(4, 0))
ttk.Label(lf5, text="前缀+3位计数(000起)\nOSS: 国家/时间戳/货号_4位随机", anchor="w", ttk.Label(lf5, text="前缀+3位计数(000起)", style="CardSub.TLabel").pack(anchor="w", pady=(4, 0))
foreground="#5F5E5A").pack(anchor="w", pady=(4, 0))
self._sku_map = {} self._sku_map = {}
self._tasks: list = [] self._tasks: list = []
self._reload_spus() self._reload_spus()
bar = ttk.Frame(self, padding=(8, 0)) # —— 操作栏 ——
bar = ttk.Frame(self, padding=(12, 6))
bar.pack(fill="x") bar.pack(fill="x")
self.run_btn = ttk.Button(bar, text="▶ 运行", style="Primary.TButton", command=self._on_run)
self.run_btn.pack(side="left")
self.fetch_btn = ttk.Button(bar, text="📥 采集热点", command=self._on_fetch) self.fetch_btn = ttk.Button(bar, text="📥 采集热点", command=self._on_fetch)
self.fetch_btn.pack(side="left") self.fetch_btn.pack(side="left", padx=(10, 0))
self.run_btn = ttk.Button(bar, text="▶ 运行", command=self._on_run) ttk.Button(bar, text="打开产物目录", command=self._open_output).pack(side="left", padx=(10, 0))
self.run_btn.pack(side="left", padx=6) ttk.Button(bar, text="打开日志目录", command=self._open_logs).pack(side="left", padx=(10, 0))
ttk.Button(bar, text="打开产物目录", command=self._open_output).pack(side="left", padx=8) ttk.Button(bar, text="清空日志", command=self._clear_log).pack(side="left", padx=(10, 0))
ttk.Button(bar, text="打开日志目录", command=self._open_logs).pack(side="left")
ttk.Button(bar, text="清空日志", command=self._clear_log).pack(side="left")
ttk.Label(self, text="运行日志:").pack(anchor="w", padx=8) # —— 运行日志(深色终端风格)——
self.log_text = tk.Text(self, height=8, state="disabled") log_card = ttk.LabelFrame(self, text="运行日志", style="Card.TLabelframe", padding=6)
self.log_text.pack(fill="x", padx=8) log_card.pack(fill="x", padx=12, pady=(4, 4))
self.log_text = tk.Text(log_card, height=7, state="disabled", wrap="word",
bg="#0F172A", fg="#E2E8F0", font=("Consolas", 9),
relief="flat", padx=8, pady=6, insertbackground="#E2E8F0")
self.log_text.pack(fill="both", expand=True)
ttk.Label(self, text="设计简报(双击查看完整提示词):").pack(anchor="w", padx=8) # —— 设计简报表格 ——
tree_card = ttk.LabelFrame(self, text="设计简报(双击查看完整提示词)",
style="Card.TLabelframe", padding=6)
tree_card.pack(fill="both", expand=True, padx=12, pady=(4, 10))
cols = ("country", "topic", "category", "risk", "score") cols = ("country", "topic", "category", "risk", "score")
self.tree = ttk.Treeview(self, columns=cols, show="headings", height=12) self.tree = ttk.Treeview(tree_card, columns=cols, show="headings", height=12)
for col, title, w, anchor in zip( for col, title, w, anchor in zip(
cols, ["国家", "热点词", "类别", "风险", "分数"], cols, ["国家", "热点词", "类别", "风险", "分数"],
[52, 340, 100, 70, 64], [70, 420, 180, 90, 80],
["center", "w", "w", "center", "center"], ["center", "w", "w", "center", "center"],
): ):
self.tree.heading(col, text=title) self.tree.heading(col, text=title)
self.tree.column(col, width=w, anchor=anchor) self.tree.column(col, width=w, anchor=anchor)
self.tree.pack(fill="both", expand=True, padx=8, pady=(2, 8)) self.tree.pack(fill="both", expand=True)
self.tree.bind("<Double-1>", self._show_detail) self.tree.bind("<Double-1>", self._show_detail)
def _log(self, s: str): def _log(self, s: str):
@@ -692,12 +809,6 @@ class App(tk.Tk):
if colors: if colors:
self.sku_list.selection_set(0) # 默认选中第一个颜色 self.sku_list.selection_set(0) # 默认选中第一个颜色
def _current_spu(self):
sel = self.spu_list.curselection()
if sel and sel[0] < len(self._spu_list):
return self._spu_list[sel[0]]["code"]
return ""
def _add_task(self): def _add_task(self):
"""把当前选中的【款号】加入任务清单(可多选、可重复新增)。 """把当前选中的【款号】加入任务清单(可多选、可重复新增)。
每个款号一条「集合」:skus=选中颜色集合(多色用逗号分隔), 每个款号一条「集合」:skus=选中颜色集合(多色用逗号分隔),
@@ -848,12 +959,30 @@ class App(tk.Tk):
self.run_btn.config(state="disabled") self.run_btn.config(state="disabled")
threading.Thread( threading.Thread(
target=fetch_keywords, target=fetch_keywords,
args=(self.country_var.get(), self.provider_var.get(), ms, self._q, args=(self.country_var.get(), "openai", ms, self._q,
self._oai_cfg()), self._oai_cfg()),
daemon=True, daemon=True,
).start() ).start()
# ---------- 运行 ---------- # ---------- 运行 ----------
def _choose_custom_dir(self):
"""选择自定义模式图片文件夹,并立即统计有效图片数量(供数量校验提示)。"""
path = filedialog.askdirectory(title="选择自定义图片文件夹(直接送多模态分析)")
if not path:
return
try:
from graph.pinterest import list_valid_images
imgs = list_valid_images(path)
n = len(imgs)
except Exception: # noqa: BLE001
n = 0
self.custom_dir_var.set(path)
if n == 0:
self.custom_dir_name_var.set(f"已选: {Path(path).name}0 张有效图片!)")
else:
self.custom_dir_name_var.set(f"已选: {Path(path).name}{n} 张有效图片)")
self._log(f"[UI] 自定义图片文件夹已选择: {path}(有效图片 {n} 张,jpg/jpeg/png/webp\n")
def _choose_template(self): def _choose_template(self):
"""选择自定义商品上传模板(.xlsx),后续 template_export 从该模板解析。 """选择自定义商品上传模板(.xlsx),后续 template_export 从该模板解析。
导入时校验「经营站点」是否与当前国家一致(如 JP → 日本站),不一致则导入失败。""" 导入时校验「经营站点」是否与当前国家一致(如 JP → 日本站),不一致则导入失败。"""
@@ -905,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
@@ -913,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
@@ -990,18 +1131,12 @@ class App(tk.Tk):
messagebox.showerror("参数错误", "加价%不能为负数") messagebox.showerror("参数错误", "加价%不能为负数")
return return
# 任务清单优先;空则用当前选中款号+颜色(保留旧行为) # 任务清单必须通过「+添加选品」显式添加后才允许运行;
# 不允许用当前选中款号+颜色兜底(否则未添加选品也能启动,会按数量扩展生成大量简报)。
tasks = list(self._tasks) tasks = list(self._tasks)
if not tasks: if not tasks:
spu = self._current_spu() messagebox.showwarning("提示", "请先添加选品(款号 → 颜色 → +添加选品)")
if not spu: return
messagebox.showwarning("提示", "请先添加选品(款号 → 颜色 → +添加选品)")
return
sel_colors = [self.sku_list.get(i).split(" ")[0] for i in self.sku_list.curselection()]
if not sel_colors:
messagebox.showwarning("提示", "请至少选择一个颜色")
return
tasks = [{"spu": spu, "skus": ",".join(sel_colors)}]
# ① 兜底校验:任务开始前检查所有款号颜色的 basemap 底图是否存在; # ① 兜底校验:任务开始前检查所有款号颜色的 basemap 底图是否存在;
# 存在缺失 → 直接报错提醒,不开始任何任务 # 存在缺失 → 直接报错提醒,不开始任何任务
@@ -1022,20 +1157,46 @@ class App(tk.Tk):
self._busy = True self._busy = True
self.run_btn.config(state="disabled", text="运行中…") self.run_btn.config(state="disabled", text="运行中…")
self.fetch_btn.config(state="disabled") self.fetch_btn.config(state="disabled")
if self.flow_var.get() == "pinterest": flow = self.flow_var.get()
# Pinterest 参考模式:独立于 Google Trends 采集链路 if flow in ("pinterest", "custom"):
# Pinterest 参考 / 自定义图片:独立于 Google Trends 采集链路
custom_dir = self.custom_dir_var.get().strip()
if flow == "custom":
# —— 自定义图片模式硬校验:必须先选文件夹;有效图片数 ≥ 选品清单总数 ——
if not custom_dir:
self._busy = False
self.run_btn.config(state="normal", text="▶ 运行")
self.fetch_btn.config(state="normal")
messagebox.showwarning("自定义图片模式", "请先点击「📁 选图片文件夹…」选择上传的图片文件夹")
return
try:
from graph.pinterest import list_valid_images, validate_custom_images
# 选品清单总数 = 每个任务按 count 扩展后的求和(与 run_pinterest_pipeline 一致)
expanded = sum(int(t.get("count") or 0) or spu_count or 1 for t in tasks)
ok, valid_n, msg = validate_custom_images(custom_dir, expanded)
except Exception as e: # noqa: BLE001
ok, valid_n, msg = False, 0, f"校验失败: {e}"
if not ok:
self._busy = False
self.run_btn.config(state="normal", text="▶ 运行")
self.fetch_btn.config(state="normal")
messagebox.showerror("自定义图片数量校验未通过", msg)
self._log("[UI] 自定义图片数量校验未通过,任务未运行: " + msg + "\n")
return
self._log(f"[UI] 自定义图片数量校验通过:有效图片 {valid_n} 张 ≥ 选品清单 {expanded}\n")
threading.Thread( threading.Thread(
target=run_pinterest_pipeline, target=run_pinterest_pipeline,
args=(countries, self.provider_var.get(), self._q, args=(countries, "openai", self._q,
tasks, spu_count, self._oai_cfg(), markup, tasks, spu_count, self._oai_cfg(), markup,
self.code_prefix_var.get().strip() or "DG", self.code_prefix_var.get().strip() or "DG",
self.template_path_var.get().strip()), self.template_path_var.get().strip(),
custom_dir if flow == "custom" else ""),
daemon=True, daemon=True,
).start() ).start()
else: else:
threading.Thread( threading.Thread(
target=run_pipeline, target=run_pipeline,
args=(countries, self.provider_var.get(), ms, self._q, args=(countries, "openai", ms, self._q,
"", "", spu_count, tasks, self._oai_cfg(), markup, "", "", spu_count, tasks, self._oai_cfg(), markup,
self.code_prefix_var.get().strip() or "DG", self.code_prefix_var.get().strip() or "DG",
self.template_path_var.get().strip()), self.template_path_var.get().strip()),
@@ -1117,7 +1278,7 @@ class App(tk.Tk):
("国家", "country"), ("热点词", "topic"), ("类别", "design_category"), ("国家", "country"), ("热点词", "topic"), ("类别", "design_category"),
("风险", "risk_level"), ("风险原因", "risk_reasons"), ("概念", "concept"), ("风险", "risk_level"), ("风险原因", "risk_reasons"), ("概念", "concept"),
("主体 motif", "motif"), ("艺术风格", "art_style"), ("配色", "color_palette"), ("主体 motif", "motif"), ("艺术风格", "art_style"), ("配色", "color_palette"),
("构图", "composition"), ("负向", "negative_prompt"), ("构图", "composition"),
("image_prompt", "image_prompt"), ("composite_prompt", "composite_prompt"), ("image_prompt", "image_prompt"), ("composite_prompt", "composite_prompt"),
]: ]:
v = b.get(k) v = b.get(k)
-294
View File
@@ -1,294 +0,0 @@
# POD 趋势感知 Agent · 对接文档
> 面向开发者/维护者的适配与扩展指南。覆盖:项目架构、**如何适配新国家**、核心流程、缓存机制、合规红线、常见问题与打包发布。
> 适用版本:dist_v67 / 源码 `pod_trend_agent/`
---
## 1. 项目概览
**目标**:为 PODPrint on Demand)出海业务自动捕捉海外热点 → 生成可打印的 T 恤印花设计 → 三图合成模特图 → 多语言标题 → 导出商品上传模板。
**技术栈**Python 3.13 · LangGraph7 节点流水线)· tkinter 桌面 UI · requests · Pillow · PyInstaller 打包
**两条运行路径**
| 路径 | 触发条件 | 行为 |
|---|---|---|
| **缓存热点模式**(日常) | `output/<国>/collected_keywords.json` 存在 | 直接用采集缓存分配热点;简报不足时**从采集缓存自动生成简报**(轻量补齐),**绝不触发 Google** |
| **完整流水线**(首次) | 完全无缓存 | `seed → fetch → filter → score → screen → prompt_build → compose → product → seed_shot → template_export → oss_upload` |
---
## 2. 目录结构
```
pod_trend_agent/
├── ui_app.py # 桌面 UI:选品/采集/运行/模板/结果表格
├── cli.py # 命令行入口
├── config.yaml # 全局配置(countries/sources/llm_screen/prompt_templates/compose/oss/product…)
├── configs/countries/<CC>.yaml# ★每国独立配置:种子词/related 种子/采集限额
├── prompts/<CC>/ # ★每国提示词:system_prompt.md + aesthetics.yaml
├── graph/
│ ├── agent.py # LangGraph 流水线编排(START→seed→fetch→…→END
│ ├── nodes/ # 各节点实现(seed/fetch/filter/score/screen/prompt/compose/product/seed_shot/template/oss
│ ├── product_batch.py # 缓存热点批量流程:分配/三级去重/review 兜底/轻量补齐
│ ├── backends/ # 图像后端:openai_image_backend / mock_image_backend
│ ├── llms/ # LLM 后端:openai_compat_backend(多模态标题)/ mock_backend
│ ├── seeds/ # 种子策略:dynamic_strategy(统一池+加权随机)/ static / holidays
│ ├── sources/ # 数据源注册表:google_trendspytrends429 兜底)/ pinterest
│ ├── product.py # 底图/模特查找(IMG_EXTS 全格式)、SKU 逻辑
│ ├── style_rules.py # 风格/配色/构图规则 + sanitize_image_prompt + review 魔改引导
│ ├── paths.py # config_root / project_root / runtime_root 路径解析
│ └── validate.py # 校验 + with_fallback 兜底装饰器
├── templates/ # 商品上传模板 + template_router.py(模板读写)
├── basemap/<款号>/<颜色>/ # 平铺底图(jpg/png…)
├── material_library/<品类>/ # 模特图(webp/jpg/png/avif…)
├── db/ # 商品数据库(sqlite
└── dist_vNN/ # PyInstaller 打包产物(含全部数据)
```
---
## 3. ★适配新国家(核心章节)
以新增 **DE(德国)** 为例,完整步骤:
### 3.1 国家代码入列
`config.yaml`
```yaml
countries:
- US
- GB
- JP
- AU
- MX
- DE # ← 新增
```
### 3.2 每国配置 `configs/countries/DE.yaml`
```yaml
# 德国专属数据源配置
trending:
enabled: true
limit: 40 # Google Trends 抓取上限
style:
enabled: true
seeds: # ★印花风格种子词(重点,越多越好,20~30 个)
- lederhosen
- alpine
- berlin street art
- bauhaus pattern
- autobahn
- ...
related:
enabled: true
seed_keywords:
- ...
exclude_topics: [] # 黑名单热点(已用过/明确不分配的词,可选)
```
**种子词选词红线**(写配置时注意):
- ❌ 会被 `review 兜底 _UNSUITABLE` 剔除的类目:`nails / recipes / lottery / weather / results / prices / jobs / mortgage / election…`(见 `graph/product_batch.py`
- ❌ 品牌/版权/真实人物:`nike / disney / margot robbie / prison break…`
- ❌ 图像策略敏感词:`skull / skeleton / blood / gun / demon…`(生成时会被 `sanitize_image_prompt` 替换,主题会变味)
- ✅ 用各国**本土流行 + 全球趋势**的印花主题(2026 参考:retro vintage / 极简 typography / 自然户外 / 手绘涂鸦 / Y2K / coquette / western / 幽默 quote / 星座塔罗 / 植物线稿)
### 3.3 每国提示词 `prompts/DE/`
- **`system_prompt.md`**screen 阶段 LLM 的角色/输出规则(风险判定、motif/art_style/color_palette/composition 四要素),**必须包含 "Return JSON…" 字样**(阿里云 MaaS 的 `response_format=json_object` 硬性要求)。
- **`aesthetics.yaml`**
```yaml
extra_style_rules: [...] # 该国风格倾向(配色/构图/审美)
extra_blacklist: [...] # 该国合规红线词(德国:纳粹符号等)
```
### 3.4 节假日 `graph/seeds/holidays.py`
`_HOLIDAYS_BY_COUNTRY` 增加国家节日(名称/月/日/规则/窗口),节日种子词自动带权重 3.0 进入统一池:
```python
"DE": [
("Oktoberfest", 9, 21, "third_saturday", 30), # 啤酒节(9 月)
("Weihnachten", 12, 25, "fixed", 25), # 圣诞节
...
]
```
### 3.5 国家模板站点校验 `ui_app.py`
`_check_template_site` 的 `country_sites` 字典加国家→站点关键词:
```python
country_sites = {
...
"DE": ("德国站", "德国"),
}
```
(模板「模版」sheet 第 1 行定位「经营站点」列 → 第 2 行站点名 → 与所选国家不匹配则**导入失败**,提示重新导入)
### 3.6 图像提示词模板 `config.yaml → prompt_templates`
`image_prompt` / `wearable_prompt` / `composite_prompt` 各加国家版本(`{motif}` 等占位符保持不变),注意:
- **提示词中不得出现任何背景描述**"isolated on … background"、"no background scene")——透明背景只由 `background="transparent"` 参数控制(官方要求)
- 不含安全规则段("no politics / no religion / no hate / no violence / no sexual"——这些词本身会触发图像 API 内容政策拦截)
### 3.7 验证清单
```bash
python cli.py -c DE --provider mock # mock 跑通全流程(不花钱)
python -c "from graph.seeds.dynamic_strategy import DynamicStrategy; ..." # 验证种子池
# UI 打开 → 切 DE → 采集 → 运行
```
---
## 4. 核心流程详解
### 4.1 完整流水线(LangGraph 7 节点)
```
START → seed(种子词) → fetch(采集) → filter(过滤) → score(打分) → screen(风险筛选+简报)
→ prompt_build(提示词) → compose(设计稿) → product(合成/标题) → seed_shot(种草图)
→ template_export(模板) → oss_upload(压缩上传) → END
```
每个节点均套 `with_fallback` 兜底(失败不中断整体流程)。
### 4.2 缓存热点模式(日常入口,`ui_app` / `run_product_batch`
```
判断:output/<国>/ 有 collected_keywords.json 或 design_briefs.json → 缓存模式
→ load 简报(简报不足 → _rebuild_briefs_from_cache 从采集缓存生成,不碰 Google)
→ assign_hotspots 分配(三级去重 + review 兜底)
→ product 节点批量处理(并发 ≤5,每 SPU 一线程)
→ 模板导出
```
### 4.3 热点分配与去重(`assign_hotspots`
| 层级 | 逻辑 |
|---|---|
| ① 热点去重 | 本批优先不同 topic(热点1-风格1、热点2-风格1…) |
| ② 风格去重 | 热点用完后自动切:热点1-风格2、热点2-风格2…(同 topic 不同 art_style |
| ③ 兜底 | 剩余未用指纹(低分热点也可);**历史已用 (topic, style) 指纹绝不重复出** |
| review 兜底 | `_UNSUITABLE` 正则剔除不适合类目(美甲/食谱/彩票/天气/比分…),日志提示 |
### 4.4 种子词机制(`dynamic_strategy.py`
- **统一池**:静态 style + 静态 related + 月份主题 + 节日 + LLM 动态 → 全部合并去重(跨类型同词权重累加)
- **权重**:节日 3.0 > 月份主题 2.0 > 静态/动态 1.0(加权随机抽取)
- **每次随机取**(加权不重复);**用完全用**(池 ≤ 需要数时全部使用,不随机限量)
- 每国独立配置(`configs/countries/<CC>.yaml`),种子缓存 `.cache/seeds/`(按 国家+provider+日期,防 Google 429
---
## 5. 缓存文件
| 文件 | 位置 | 作用 |
|---|---|---|
| `collected_keywords.json` | `output/<国>/` | 采集热点池(关键词列表) |
| `design_briefs.json` | `output/<国>/` | 简报池(topic/motif/risk/score/image_prompt…) |
| `used_designs.json` | `output/<国>/` | 已用 (topic, art_style) 指纹(成功生成才写入) |
| `.cache/seeds/` | 项目根 | 种子词日缓存(防 429 重采集) |
| `.cache/llm_screen/` | 项目根 | LLM 筛查缓存 |
> 注意:**失败产品不写 used_designs**(合成/设计失败的,热点下次仍可用)。
> 已用过不要的热点:直接从 `collected_keywords.json` / `design_briefs.json` 物理删除即可(如 8-22 的 18 个 GB 热点处理方式)。
---
## 6. 合规红线(最重要)
1. **review 判定**screen 阶段 LLM 判定疑似商标/版权/真实人物 → `risk_level=review`;openai 模式下生成设计时**自动注入原创化魔改引导**(`REVIEW_REBRAND_HINT`:只做风格参考、禁止复刻 logo/商标/角色、换名换细节)
2. **提示词清洗** `sanitize_image_prompt`(生成设计前必过):
- 删除一切背景描述(透明背景由参数控制)
- 删除内容策略触发段(`any text must be safe: no politics/religion/hate/violence/sexual…`
- 21+ 个敏感词安全替换(skull→smiley mascot、gun→star…)
3. **各国红线**:美国慎政治/宗教;德国慎纳粹符号(`aesthetics.yaml extra_blacklist` 扩展);中东整体保守;真实人物一律不进
4. **review 兜底 `_UNSUITABLE`**:不适合类目自动剔除(分配源头拦截)
---
## 7. 图像生成与合成
### 7.1 设计稿生成(`compose` / `product` 的 `generate`
- **透明背景**`background="transparent"` 参数(compose/product 配置均已设);**提示词不得提背景**
- 并发:compose 并发生成(`compose.design_workers` 默认 5);product 阶段每 SPU 一线程,并发上限 5
- 底图/模特/设计 >2MB → 自动压缩到 <2MB 再上传网关(`_shrink_blob_to_2mb`
### 7.2 三图合成(`product` 的 `print`mark=1
- 上传顺序:**图1 模特图 → 图2 印花设计稿 → 图3 衣服平铺底图**(与提示词角色一一对应)
- 图3 只提取衣服本身底色与面料材质(忽略平铺图背景/桌面/场景)
- 合成失败 → 退避重试 3 次(5/20/40s);仍失败 → 跳过该产品(不生成标题、不写模板)
- 网关处理约 2-6 分钟属正常(重负载 img2img)
---
## 8. 模型与网络配置(`config.yaml`
```yaml
llm_screen: # LLMscreen/种子/标题共用)
model: "qwen3.7-..." # ★标题生成需视觉模型(qwen-vl 系列);纯文本模型会 400
base_url: "https://...maas.aliyuncs.com/compatible-mode/v1"
api_key: "sk-..." # 或环境变量 LLM_API_KEY
compose: # 设计稿图像网关
backend: "openai"
model: "gpt-image-2" # 支持 background=transparent
base_url: "https://api.tofastcode.xyz"
background: "transparent"
product:
backend: "mock" # openai / mock
background: "transparent"
concurrency: 5 # 合成并发上限(默认 min(任务数,5))
```
**代理策略**:模型调用(LLM + 图像)一律直连(`NO_PROXY` + 环境变量级 `no_proxy=*`,用户挂 VPN 也不走代理);**Google 采集(pytrends)走系统代理**(需要 VPN)。
---
## 9. 打包发布(每版流程)
```bash
# 1. 打包(不跑 self-test:会撞 Google 429,浪费 7-10 分钟)
pyinstaller -y PODTrendAgent.spec --distpath dist_vNN --workpath build_cleanNN
# 2. 补齐 dist 数据(必须,否则 exe 缺资源)
cp -r templates db basemap material_library configs prompts .cache dist_vNN/
cp config.yaml dist_vNN/
# 3. 拷贝最近缓存(output/<国>/*.json),避免 exe 首次运行重新采集
# 4. 验证:find_first_model_folder 能查到模特(material_library 拷贝不完整是常见坑)
```
---
## 10. 常见问题(FAQ
| 现象 | 根因 | 解决 |
|---|---|---|
| 有缓存还去采集(Google 429 刷屏) | UI 判断只看 design_briefs,忽略 collected | v60+ 已修:有 collected 即缓存模式,绝不触发 seed/fetch |
| Google 429 / SSL EOF | 限流 | 换 VPN 节点;已采集后运行走缓存不再碰 Google |
| 三图合成全部超时 | 并发过高压垮网关 / 底图过大 | 并发上限 5 + 输入图压缩 <2MB + 退避重试 |
| 生成设计全是商标侵权 | review 简报未魔改 | v57+ 自动注入 REVIEW_REBRAND_HINT |
| 内容政策拦截频繁 | 提示词含敏感词/安全规则段/背景描述 | sanitize_image_prompt 自动清理 |
| 设计稿不透明(RGB 白底) | product 后端未配 background | `product.background: transparent` |
| 标题 400 | 模型不支持图片输入(非视觉模型) | 换 qwen-vl 系列视觉模型 |
| 模特找不到 | material_library 拷贝不全 / avif 不识别 | 全量拷贝;IMG_EXTS 已含 avif/gif/tiff |
| 生成的设计 6 个重复 | compose 只生成 safe 简报的设计 | v48+ 为全部简报(含 review)生成设计 |
---
## 11. 开发环境
```bash
python -m venv .venv
.venv/Scripts/pip install -r requirements.txt # 或 uv syncuv.lock 已锁定)
# 环境变量:LLM_API_KEY / LLM_BASE_URL / OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET
```
**对接新国家一句话总结**`configs/countries/<CC>.yaml`(种子词)+ `prompts/<CC>/`(提示词)+ `holidays.py`(节日)+ `config.yaml countries 列表` + UI 国家下拉 + 模板站点映射 → mock 跑通 → 真 LLM 验证。