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 排除测试产物
This commit is contained in:
2026-08-28 10:28:35 +08:00
parent 685b7b0862
commit 2a96ec0870
28 changed files with 1187 additions and 729 deletions
+4
View File
@@ -21,6 +21,10 @@ logs/
# Pinterest 登录态(首次手动登录后缓存,不入库)
pinterest_scraper/.chrome_session/
# 测试产物
_probe_out/
test_yunfei_concurrency.py
# 系统
.DS_Store
Thumbs.db
+27 -1
View File
@@ -55,6 +55,9 @@ START → pinterest_init → pinterest_search → pinterest_scrape → pinterest
**图池机制**:爬取的图片注册到 `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`(每搜索词简报上限)、
@@ -124,8 +127,26 @@ pod_trend_agent/
设计稿生成后全局 MD5 去重(`.cache/global_design_md5.json`),跨国家生效。
5. **多模态分析**:大图先压缩(max 1024px / 1.5MB)再送 LLM,产出原创设计简报(motif/风格/配色/构图 +
risk_levelsafe/review/blocked)。blocked 拦截、review 加原创化魔改引导、safe 直接生成。
每张图由多模态模型判定 `suitable_for_print`(是否适合做印花),False 的简报在节点层被过滤丢弃,
且该图 md5 立即拉黑,不进入生成流程。
6. **并发生成**:简报推入 `PinterestPipeline`,后台线程池(`product.concurrency`,默认 5)逐条
生成 设计稿 → 三合一 → OSS 上传 → 种草图,边分析边生成,不等全部分析完。
每完成一个产品立即追加落盘到 `output/<country>/<ts>/products_pending.jsonl`(中断/崩溃也不丢已
完成产品),全部完成后统一合并写模板。
## 生成可靠性 / 错误处理
- **致命 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 次,避免空响应/超时风暴触发限流。
- **yunfei / 标准 OpenAI 网关适配**`compose.execution_mode``background` 参数默认不再传入——
yunfei 等网关不认识它们会返回空 body;`base_url` 需带 `/v1` 前缀(如
`https://img.yunfei.best/v1`),否则请求错误路径得到空响应。
- **模特分配**`material_library/<品类>` 内的合格模特图(3:4 比例过滤)按**任务序号**独立随机,
同一 SPU 的多款不再共用同一张模特。每张种草图随机取不同模特特征。
## 模特性别分组
@@ -146,10 +167,15 @@ pod_trend_agent/
## 商品上传模板导出
生成产品图后,从 db 读该 SPU/SKU 信息(材质/成分/图案/领型/面料/克重/印花类型/尺码表/边长/包装重量…),
`templates/template_router.py` 的路由函数填入商品上传模板,输出 `<SKU>_已填写.xlsx`
`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%)`);
+9 -6
View File
@@ -46,15 +46,18 @@ pinterest:
seed_sample: 40 # 每次从国家种子池随机抽取多少个种子词给 LLM
max_used_terms_in_prompt: 100 # 已用搜索词最多注入 LLM 提示词的个数(防 token 超限)
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_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次补齐到目标所需/每词简报上限,让 pipeline 队列时刻满并发
analyze_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次最多分析 max_designs 张;搜集完一批→生成一批简报,不够用了才主动采集
max_designs: 10 # 每个搜索词最多生成多少个设计简报
ref_images_per_design: 1 # 生图时每个设计附带几张爬取图作为参考(发给生图模型)
supply_attempts: 3 # 设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数
err400_limit: 15 # 每个种子词累计 400(且错误含「内容/图片」)超限后放弃该种子词(多模态+生图合计)
scrape_concurrency: 1 # 同时爬取几个搜索词(共享 .chrome_session 登录态,Chrome 单例,必须=1)
headless: false # 爬取时是否无头(false=显示 Chrome 窗口,首次需手动登录)
proxy: "" # 图片下载代理(空=自动探测系统代理/VPN,默认走代理;配了如 http://127.0.0.1:7890 则用指定代理)
login_check: true # 爬取前静态检测 Pinterest 登录态(只读 .chrome_session cookies,不启动 Chrome;未登录则跳过本轮爬取并告警)
login_wait: false # 运行时检测到未登录时:false=跳过并告警(不阻塞);true=弹出 Chrome 等待手动登录
# 跨源融合权重(按 source 标签,无需和为 1)
# 已下调 gt_trending(泛国家热点只作微弱信号),主力偏向 style+related(可印花型词)。
@@ -162,12 +165,12 @@ RULES:/n1.底色锁定:从图3提取衣服底色与面料,最终合成中必须1
# backend 留空则不生成设计稿(product 节点会自行回退生成)。
compose:
backend: "openai" # 生成设计稿的图像后端:openai(需 api_key/ mock(占位);留空=不生成
api_key: "sk-ef1f251f059f9c2f8cc397de4a84e96d65775e1f2d32bb13b69b23dafc24d111"
base_url: "https://api.tofastcode.xyz" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404
api_key: "sk-1CicJFACR4JCeLe6LTdsqSd0ffSrybMgmdfRiNfIlKok8MbS"
base_url: "https://img.yunfei.best/v1" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404
model: "gpt-image-2"
size: "1536x2048" # 合成图/种草图等非设计图的统一尺寸(3:4)
design_size: "1024x1024" # 纯印花设计稿尺寸(1:1
background: "transparent" # 生成透明背景 PNG(gpt-image 系列支持);留空=默认背景
execution_mode: "" # 网关异步/同步模式参数(仅 ai-media 等网关需要,如 "sync");yunfei 等标准 OpenAI 网关不认识该参数,留空=不传
seed: 0 # 随机种子:>0=固定(相同 seed 可复现,需网关支持);0/留空=每次随机
design_count: 1 # 用前 N 个 safe 简报生成设计稿(product 取第一个复用)
design_workers: 5 # 设计稿并发生成线程数(仅热点采集模式 compose 节点用;Pinterest 模式走 pipeline 总并发 product.concurrency
@@ -208,9 +211,9 @@ product:
sku_code: "" # 颜色编码(留空自动选该款第一个有底图的);多个颜色用逗号分隔(如 "DG015-VT01,DG015-DARK HEATHER"
spu_per_color: # 留空自动判定:单色=每色一SPU;多色=单SPU多色(UI 不选择模板模式);CLI --single-spu 可显式覆盖
markup_percent: 0 # 加价百分比(按 SKU 最低价,供后续定价使用)
suggested_price_ratio: 30 # 建议售价比例(%):建议售价 = 申报价格 × (1+此比例/100);模板「建议售价」必填时才填
code_prefix: "DG" # 货号前缀(OSS 上传 key:{国家}/{时间戳}/{前缀+3位计数}_{4位随机}.jpg000 起最多 999
backend: "mock" # openai / mock / 留空=跳过生成
background: "transparent" # 设计稿透明背景(product 后端生图时也传 background=transparent
template_dir: "templates" # template_router.py 所在目录(项目内自包含)
template_path: "templates/商品上传模版.xlsx" # 商品上传模板(可改为自由上传)
concurrency: 5 # Pinterest 模式 pipeline 总并发(设计生成→三合一→种草图→标题 同一管道串行,总并发=此值)
+57 -44
View File
@@ -1,75 +1,88 @@
# 模特特征库 —— 跨境电商基础款服装展示专用(已剔除外部穿搭与夸张妆造干扰
# 模特特征库 —— 跨境电商年轻款服装展示专用(全库年龄20-33岁
# 占位符:[模特特征] 随机取用
# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装。
# 按性别分组:female=女模 / male=男模
# 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机。
# 取用规则:
# 1. 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机
# 2. 如需同一商品多图保持同一模特(一景多动作),首次随机取到后请固定复用,不要每张图重新随机。
# 3. 站点适配:北美/欧洲站优先「欧美系+多元族裔系」,东南亚站优先「东南亚裔」,日韩站优先「亚洲系」。
# 描述规范:每条 = 年龄 + 族裔/风格标签 + 发型发色 + 肤色肤质 + 气质表情(字段顺序统一,出图更稳定)。
# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装,不描述下半身。全库限定年轻人群。
model_features:
female:
# ========== 女模 - 亚洲/清新/通勤类(适合基础款、纯色T恤 ==========
- "22岁亚洲清新女学生,通透伪素颜,黑直发,清瘦骨感"
- "25岁日韩系通勤女模,元气通透底妆,棕色微卷发,亲切感"
- "27岁亚洲极简风女模,冷白皮,无瑕光泽肌,利落黑色短发"
- "29岁亚洲轻熟女模,骨相美,微雾感底妆,气质温婉"
# ========== 1. 亚洲系(适合基础款/日韩风/通勤 ==========
- "22岁亚洲清新系女模,黑长直发,通透伪素颜,清瘦骨感,邻家亲和感"
- "25岁日韩系通勤女模,棕色微卷发,元气通透底妆,亲切微笑感"
- "27岁亚洲极简风女模,利落黑色短发,冷白皮,无瑕光泽肌,知性气质"
- "29岁亚洲轻熟女模,微卷深棕发,微雾感底妆,骨相精致,温婉气质"
- "31岁亚洲职场御姐女模,中分黑长发,干净底妆,干练自信气场"
# ========== 女模 - 欧美/阳光/甜心类(适合印花T恤度假风) ==========
- "20岁欧美甜心女模,金发碧眼,阳光加州感,健康小麦色肌肤"
- "23岁法式慵懒女模,法式刘海微卷棕发,通透红唇"
- "27岁欧美自然系女模,淡雀斑,大地色淡妆,健康好气色" # 新增
- "21岁迈阿密热辣女孩,金棕色大波浪,高光立体修容,健康古铜色肌肤"
- "24岁北美学院风女模,高马尾,通透元气妆,白皙肌肤"
- "34岁北美宝妈系女模,自然淡妆,栗色长发,亲切温暖的微笑" # 新增
# ========== 2. 欧美阳光(适合印花T恤/度假风) ==========
- "20岁欧美甜心女模,金色长直发,碧眼,健康小麦色肌肤,阳光加州感笑容"
- "23岁法式慵懒女模,法式刘海微卷棕发,通透底妆,柔和红唇,松弛气质"
- "27岁欧美自然系女模,浅棕长发,淡雀斑,大地色淡妆,健康好气色"
- "21岁迈阿密热辣女孩,金棕色大波浪,健康古铜色肌肤,立体修容,明媚笑容"
- "24岁北美学院风女模,高马尾,白皙肌肤,通透元气妆,青春活力感"
- "26岁北美邻家系女模,栗色长发,自然淡妆,亲切温暖笑容"
# ========== 女模 - 欧美/街头/个性类(适合街头风、美式复古印花) ==========
- "22岁纽约下城区酷女孩,挑染发色,清冷厌世感,苍白质肌肤"
- "28岁欧美高级中性模,凌乱短发,冷白皮,骨感清冷"
- "26岁拉美混血风女模,深邃眼窝,野生挑眉,健康小麦色肌肤"
- "25岁Y2K千禧辣妹,银色挑染发,清透裸妆,微胖立体感"
# ========== 3. 欧美街头系(适合美式复古/街头印花) ==========
- "22岁纽约下城区酷女孩,灰紫色挑染发,苍白冷感肌肤,酷感眼神"
- "28岁欧美高级中性风女模,凌乱金色短发,冷白皮,骨感清冷高级脸"
- "26岁拉美混血风女模,深棕长卷发,深邃眼窝,健康小麦色肌肤,野性眉形"
- "25岁Y2K千禧辣妹,银色挑染发,清透裸妆,微胖立体感,个性张扬"
# ========== 女模 - 极简/性冷淡/高街(适合纯色基础款卫衣) ==========
- "30岁北欧极简风女模,冷白皮,无瑕哑光肌,银色利落短发"
- "29岁纽约高街潮流女模,冷感通透妆,深棕色直发,疏离眼神"
# ========== 4. 极简高街(适合纯色基础款/卫衣) ==========
- "30岁北欧极简风女模,银色利落短发,冷白皮,无瑕哑光肌,疏离高级感"
- "29岁纽约高街潮流女模,深棕色直发,冷感通透妆,清冷眼神"
- "31岁哥本哈根极简风设计师女模,利落黑色短发,黑白灰冷感气质"
- "28岁法式先锋艺术系女模,单边耳坠,冷感素颜,深金色发"
- "28岁法式先锋艺术系女模,深金色长发,冷感素颜,单边耳坠,艺术家气质"
# ========== 女模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备 ========== # 新增分组
# ========== 5. 多元族裔系(北美/欧洲站覆盖 ==========
- "26岁非裔自然卷女模,健康深色肌肤,光泽感底妆,明快笑容"
- "29岁非裔都市丽人女模,利落编发,精致底妆,自信干练气场"
- "24岁印度裔女模,浓密黑长发,深邃大眼,暖调蜜糖色肌肤"
- "27岁拉丁裔阳光女模,栗色卷发,蜜色肌肤,热情爽朗笑容"
- "32岁中东裔优雅女模,微卷黑发,轮廓深邃,端庄知性气质"
- "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容" # 大码:覆盖plus size市场
- "25岁东南亚裔清甜女模,黑长直发,蜜糖色肌肤,元气亲和笑容"
# ========== 6. 大码系(Plus Size市场) ==========
- "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容"
- "30岁欧美大码甜心女模,金色微卷发,健康白皙肌肤,自信明媚笑容"
male:
# ========== 男模 - 亚洲/盐系/清新类(适合基础款日韩 ==========
- "23岁日系盐系男模,单眼皮,清瘦骨感,柔软黑发"
- "25岁亚洲清冷系男模,冷白皮,碎发自感,下颌线清晰"
# ========== 1. 亚洲系(适合基础款/日韩 ==========
- "23岁日系盐系男模,柔软黑发,单眼皮,清瘦骨感,干净少年感"
- "25岁亚洲清冷系男模,碎发自感,冷白皮,下颌线清晰,清冷气质"
- "28岁亚洲轻熟男模,干净利落的短发,健康肤色,稳重气质"
- "21岁亚洲阳光男大,通透底妆,微卷黑发,清爽笑容"
- "21岁亚洲阳光系男模,微卷黑发,通透底妆,清爽笑容"
- "26岁亚洲运动系男模,利落黑色短发,健康小麦色肌肤,阳光笑容"
# ========== 男模 - 欧美/街头/运动(适合美式复古街头印花) ==========
# ========== 2. 欧美街头/运动(适合美式复古/街头印花) ==========
- "20岁加州冲浪系男模,阳光金发,麦色健康肌,清爽笑容"
- "24岁欧美健身教练型男,肌肉线条分明,小麦色肌肤,硬朗骨相"
- "22岁纽约街头滑板男模,脏辫,清冷厌世感,苍白肌肤"
- "27岁美式机车硬汉男模,粗犷廓,络腮胡,硬挺气质"
- "35岁北美成熟奶爸系男模,浅胡茬,栗色短发,温暖亲切笑容" # 新增
- "22岁纽约街头滑板男模,深色脏辫,苍白肌肤,酷感眼神"
- "27岁美式机车男模,粗犷廓,络腮胡,年轻硬朗气质"
- "26岁北美暖男系男模,浅胡茬,栗色短发,温暖亲切笑容"
# ========== 男模 - 欧美/商务/雅痞(适合重磅纯色T恤卫衣) ==========
- "25岁英伦雅痞型男,复古油头,短胡茬,熟男气质"
- "30岁华尔街商务精英男模,背头,沉稳内敛气质,干净皮肤"
- "28岁意大利型男,深邃五官,意式凌乱卷发,健康肌肤"
- "32岁法式儒雅大叔,微卷发,金丝眼镜,温润如玉气质"
# ========== 3. 商务/雅痞(适合重磅纯色T恤/卫衣) ==========
- "25岁英伦雅痞型男,复古油头,短胡茬,痞帅气质"
- "30岁华尔街商务精英男模,背头,干净皮肤,沉稳内敛气质"
- "28岁意大利型男,意式凌乱卷发,深邃五官,健康肌肤"
- "26岁法式文艺男模,微卷发,金丝眼镜,温柔书卷气"
# ========== 男模 - 极简/暗黑/高街(适合纯色基础款暗黑风) ==========
# ========== 4. 极简高街(适合纯色基础款/暗黑风) ==========
- "25岁欧美高冷男模,银白发色,苍白冷白皮,锋利眼神"
- "29岁柏林暗黑系青年,苍白皮肤,冷感眼神,黑色直发"
- "29岁柏林暗黑系青年,黑色直发,苍白皮肤,冷感眼神"
- "26岁东欧赛博风男模,高颧骨,清冷骨相,浅色唇"
- "33岁北欧性冷淡风男艺术家,银色短发,疏离眼神,极简气质"
# ========== 男模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备 ========== # 新增分组
# ========== 5. 多元族裔系(北美/欧洲站覆盖 ==========
- "27岁非裔阳光型男模,健康深色肌肤,利落寸头,爽朗笑容"
- "31岁非裔商务型男模,修剪整齐的短须,挺拔身姿,可靠稳重气场"
- "26岁拉丁裔热情男模,微卷黑发,蜜色肌肤,明亮笑容"
- "29岁印度裔斯文男模,黑短发,浓眉,温和儒雅气质"
- "30岁中东裔深邃男模,黑色微卷发,轮廓立体,沉稳内敛"
- "33岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容" # 大码:覆盖plus size市场
- "25岁东南亚裔阳光男模,黑色短发,蜜色肌肤,清爽亲和笑容"
# ========== 6. 大码系(Plus Size市场) ==========
- "29岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容"
- "27岁欧美大码阳光男模,浅络腮胡,魁梧体型,憨厚亲切笑容"
+76 -74
View File
@@ -1,174 +1,176 @@
# 种草图提示词模板库 —— 跨境电商详情页专用版(稳定出图+零遮挡强约束+得体姿态+全亮调场景版)
# 种草图提示词模板库 —— 跨境电商上衣专用版(服装主体构图+印花中心+零遮挡强约束+得体姿态+全亮调场景版)
# 占位符:[商品名称]、[材质]、[模特特征]、[服装风格]
# 摄影参数说明: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:
# ==========================================
# 一、 高转化白底/灰底主图区(聚焦正面印花)
# ==========================================
- name: "纯白背景正面展示"
- name: "纯白背景正面服装主体展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,身体正对镜头微侧15度,眼神直视镜头,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部位于画面最上沿且占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔自然,肩部放松下沉,双手自然垂放在身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白无缝背景纸前,身体正对镜头微侧15度,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
- name: "高级浅灰背景展示"
- name: "高级浅灰背景服装主体展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔自然,肩部放松,双手自然垂或单手插兜姿态收尾于身体两侧,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。
- name: "纯白背景双手展示下摆"
- name: "纯白背景下摆与版型展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手背身后或自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景半身构图,模特站在纯白背景纸前,眼神直视镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,重点同时覆盖胸前印花与下摆缝线,二者均须完整清晰;头部占比不超过画面高度的四分之一,五官弱化不做强调,禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,双手背身后或自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,身体正对镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺及宽松版型。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
- name: "低饱和克莱因蓝背景棚拍"
- name: "低饱和克莱因蓝背景服装主体棚拍"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特在低饱和克莱因蓝的无缝背景纸前摆出具有张力的站姿,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔有张力,肩线平直,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特在低饱和克莱因蓝的无缝背景纸前,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
- name: "纯白背景微动态走动展示"
- name: "纯白背景微转体动态展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,呈现正对镜头的微动态走动状态,整体呈现[服装风格]的调性,展示衣服正面印花在动下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身向一侧微转约30度后回望镜头方向,肩部放松,双手自然垂放于身体两侧,转动幅度不得使印花侧转到画面边缘之外,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白无缝背景纸前,呈现轻微转体的微动态,展示衣服正面印花在动下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
# ==========================================
# 二、 面料与印花特写区(打消购买顾虑)
# ==========================================
- name: "纯白背景领口细节特写"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示衣服的领口、袖口等工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装细节构图:镜头聚焦领口与上胸区域,取景范围为鼻底以下至腰部以上,画面主体是这件[商品名称]的领口罗纹、缝线工艺、肩部做工与上胸区域的印花上缘;模特面部仅保留唇部以下的局部作比例陪衬,禁止以五官为画面焦点,禁止把镜头对准脸部,禁止裁切所要展示的衣服细节部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特肩部放松下沉,双手不在画面内,绝对不可遮挡衣服任何部位。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,画面聚焦展示衣服的领口工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,85mm镜头,f/2.0光圈,8k分辨率。
- name: "明亮室内逆光面料透气质感展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,边缘形成强烈的轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是被逆光照透的面料与胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,在衣服边缘形成柔和轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。
- name: "双手拉扯领口弹性展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捏住领口两侧向外轻拉展示弹力,绝对不可将手放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,50mm镜头,f/2.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装细节构图:镜头聚焦领口与上胸区域,取景范围为鼻底以下至腰部以上,画面主体是双手轻拉领口的拉伸状态、领口罗纹回弹纹理与上胸区域的印花上缘;模特面部仅保留唇部以下的局部作比例陪衬,禁止以五官为画面焦点,禁止把镜头对准脸部,禁止裁切所要展示的衣服细节部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸。【动作硬约束】模特双手捏住领口左右两侧边缘向外轻拉展示弹力,手部仅接触领口边缘线,绝对不可移向胸口中央遮挡印花。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,85mm镜头,f/2.8光圈,8k分辨率。
- name: "低头凝视正面印花特写"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特站在纯白背景纸前,微微低头凝视衣服胸前的印花图案,整体呈现[服装风格]的调性,画面以斜俯视角度聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;模特微微低头使视线落在胸前印花上,头部随低头角度自然变小并略带透视缩短,成为氛围陪衬,画面的绝对视觉中心依然是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特微微低头注视胸前印花,头发不得垂落遮盖肩部与印花区域,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡衣服。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,85mm镜头,f/2.0光圈,8k分辨率。
# ==========================================
# 三、 居家生活场景区(营造松弛感与舒适度)
# ==========================================
- name: "居家沙发坐姿场景"
- name: "居家沙发服装主体坐姿场景"
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: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在高层公寓明亮的落地窗前,身体正前方对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在高层公寓明亮的落地窗前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透,衣服受光充足色彩还原准确。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
- name: "周末清晨床头坐姿场景"
- name: "周末清晨床头服装主体坐姿场景"
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: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手操作厨具,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特站在现代开放式厨房中岛台,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止围裙等其他服饰遮挡衣服。【动作硬约束】模特站在中岛台侧面,上半身放松自然,双手轻搭台面边缘远离胸口,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮的现代开放式厨房中岛台,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
- name: "周末居家阅读场景"
- name: "周末居家阅读服装主体场景"
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: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的城市林荫道上,身体正对镜头微侧,微微低头微笑,整体展现[服装风格]的氛围,凸显[材质]在自然行动中的透气与百搭。午后阳光透过树叶洒下明亮光斑,背景为高度虚化的过往行人与都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,35mm镜头,f/1.7大光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止强烈树影大面积压暗衣服。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光充足的城市林荫道上,身体正对镜头微侧,表情放松带浅笑,整体展现[服装风格]的氛围,凸显[材质]在自然光照下的透气与百搭。午后阳光透过树叶洒下明亮光斑,光斑只落在背景上不打在衣服表面,衣服受光均匀色彩准确。背景为高度虚化的都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,50mm镜头,f/1.7大光圈,8k分辨率。
- name: "都市街头OOTD站姿展示"
- name: "都市街头OOTD服装主体展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手拿咖啡杯放在腰侧,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的现代商业街区外墙前,身体正对镜头微靠在墙上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身微靠墙面,肩部放松,咖啡杯持有手收在腰侧画面最下沿,另一只手自然垂放,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光明媚的现代商业街区外墙前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,光线均匀打亮衣服正面,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
- name: "天台蓝天白云站姿场景"
- name: "天台蓝天白云服装主体场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光,无死角的展现服装颜色。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/2.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光均匀打在衣服上,无死角的还原服装真实颜色。画面比例为3:4。徕卡Q2摄影质感,50mm镜头,f/2.8光圈,8k分辨率。
- name: "自然公园站姿外景"
- name: "自然公园服装主体站姿外景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的公园草坪前,微风拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光,画面明亮通透,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止衣物被风吹乱遮盖印花。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光充足的公园绿植前,微风拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光板补光,衣服受光均匀色彩鲜亮,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。
- name: "街头滑板站姿场景"
- name: "滑板公园服装主体站姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚踩在滑板上,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的滑板公园边缘,身体正对镜头,微风吹拂发丝,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮阳光形成侧逆光,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/1.7光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光明媚的滑板公园,身体正对镜头,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮阳光形成轻微侧光并配合反光板给衣服暗部补光,保证印花区域无死黑,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,50mm镜头,f/1.7光圈,8k分辨率。
- name: "林荫道骑行停靠场景"
- name: "林荫道骑行服装主体场景"
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: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在细软的沙滩上,海风吹起衣摆,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,画面明亮高调,背景为高度虚化的蔚蓝海水、白沙滩与远处海平线。画面比例为3:4。索尼A7R4画质,35mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在细软的沙滩上,海风轻扬发丝,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,衣服过曝得到良好控制,色彩鲜明通透,背景为高度虚化的蔚蓝海水与远处海平线。画面比例为3:4。索尼A7R4画质,50mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。
- name: "露营帐篷坐姿场景"
- name: "露营帐篷服装主体坐姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手端着搪瓷杯放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的自然光,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,35mm镜头,f/1.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特端坐于露营椅上,上半身自然挺直,搪瓷杯收在腰腹高度的画面下沿,另一只手自然垂放,绝对不可举杯遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的漫射自然光,衣服受光均匀色彩还原准确,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,50mm镜头,f/1.8光圈,8k分辨率。
- name: "浅水沙滩站姿场景"
- name: "海边浅滩服装主体站姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在浅水沙滩边,海风吹起衣摆,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与飘逸感。光线为正午到下午的明亮阳光,海面反光形成自然补光,背景为高度虚化的蔚蓝大海、白沙滩与天空。画面比例为3:4。夏日清透色彩,高饱和度,35mm镜头,f/2.0光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止海水打湿变色衣服。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在海边浅滩旁,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈感。光线为下午的明亮阳光,海面反光形成天然柔和补光打亮衣服正面,背景为高度虚化的蔚蓝大海与天空。画面比例为3:4。夏日清透色彩,高饱和度,50mm镜头,f/2.0光圈,8k分辨率。
- name: "热带绿植温室站姿场景"
- name: "热带绿植温室服装主体场景"
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: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代美术馆纯白外墙前,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,55mm镜头,f/2.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特背靠在明亮的现代美术馆纯白外墙前,气质清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,墙面反光为衣服提供均匀补光,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
- name: "山顶站姿场景"
- name: "山顶服装主体站姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在山顶的岩石上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光,画面通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,35mm镜头,f/2.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在山顶的岩石平台上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光并配合天空的自然柔光罩效果,衣服受光均匀色彩通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,50mm镜头,f/2.8光圈,8k分辨率。
# ==========================================
# 五、 工作/通勤场景区(展现日常实用度)
# ==========================================
- name: "办公场景坐姿展示"
- name: "办公场景服装主体坐姿展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手放在桌上或键盘上不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮的极简办公桌前,身体正对镜头微侧,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,背景为高度虚化的电脑屏幕与办公桌绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止桌面物品升高遮挡衣服下摆。【动作硬约束】模特端坐于办公桌前,上半身自然挺直微侧,双手平放键盘或桌面上远离胸口,屏幕与桌面位于画面最下沿之外,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮的极简办公桌前,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,光线均匀铺满衣服正面,背景为高度虚化的办公环境与绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。
- name: "咖啡馆坐姿场景"
- name: "咖啡馆服装主体坐姿场景"
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: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捧书放在腰间不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在明亮的高大木质书架前翻阅一本精装书,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止书本遮挡衣服。【动作硬约束】模特上半身挺拔微侧,持书的手将书收在腰腹高度的画面下沿视线瞥向书页,另一只手自然垂放,绝对不可将书举起靠近胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮的高大木质书架前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,射灯角度经过调整避免在衣服上留下脸部阴影,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。
- name: "复古家具店站姿场景"
- name: "复古家具店服装主体站姿场景"
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: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在灯光柔和明亮的现代艺术画廊内,侧身凝视一幅巨大的抽象画后转过头来正对镜头,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业顶光灯打出柔和的漫反射,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,35mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特由侧身观展状态转回正对镜头,上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在灯光柔和明亮的现代艺术画廊内,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业顶光灯打出均匀漫反射铺满衣服正面,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,50mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。
- name: "超市货架站姿场景"
- name: "超市货架服装主体站姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手推车不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特推着购物车或站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,姿态俏皮个性,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源打亮环境,背景为高度虚化的彩色商品与灯轨。画面比例为3:4。赛博潮流摄影,35mm镜头,f/1.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止彩色货架光线污染衣服色调。【动作硬约束】模特上半身挺拔,扶购物车的手位于画面最下沿,另一只手自然垂放,姿态俏皮个性,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源仅照亮背景,模特所在的前景由柔和补光灯独立打亮,确保衣服色彩不受环境色影响而偏色,背景为高度虚化的彩色商品。画面比例为3:4。赛博潮流摄影,50mm镜头,f/1.8光圈,8k分辨率。
# ==========================================
# 六、 情绪氛围与高亮特色场景区(提升品牌调性)
# ==========================================
- name: "明亮工业风咖啡馆站姿展示"
- name: "明亮工业风咖啡馆服装主体展示"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在明亮工业风咖啡馆的纯白水泥墙前,眼神清冷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,背景为高度虚化的明亮通道与绿植。画面比例为3:4。赛博潮流电商摄影,55mm镜头,f/2.0光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮工业风咖啡馆的纯白水泥墙前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,衣服正面受光均匀立体,背景为高度虚化的明亮通道与绿植。画面比例为3:4。潮流电商摄影,85mm镜头,f/2.0光圈,8k分辨率。
- name: "录音棚站姿场景"
- name: "录音棚服装主体站姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手调音不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特戴着监听耳机站在专业麦克风前调音,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用录音棚明亮的暖色聚光灯与整体白光对比,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止耳机线垂落遮挡印花。【动作硬约束】模特佩戴监听耳机站立于麦克风架旁,麦杆收至脸侧避开胸口区域,一只手轻轻扶住耳机耳罩位于画面侧缘,另一只手自然垂放,绝对不可让手部或设备遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特在明亮的录音棚内,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用明亮的暖色聚光灯配合正面柔光板补光,确保印花区域通透明亮,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
- name: "暖光清吧坐姿场景"
- name: "暖光清吧服装主体坐姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手把玩酒杯放在吧台上不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在氛围感清吧的吧台前,眼神微醺迷离,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄色点光源与背景微弱的白光对比,背景为高度虚化的酒瓶阵列与杯光交错。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止酒杯遮挡衣服。【动作硬约束】模特端坐于吧台前,上半身自然挺直微侧,酒杯托在手心置于吧台面的画面下沿高度,远离胸口,另一只手自然垂放,绝对不可举杯遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在氛围感清吧的吧台前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄主光源配合正面白色补光板,衣服正面曝光充足色彩不偏橙,背景为高度虚化的酒瓶阵列与明亮灯带。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。
- name: "日落黄昏街头站姿场景"
- name: "日落黄昏街头服装主体站姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手插兜不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特倚靠在城市明亮的天桥栏杆旁,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏下的视觉效果与[材质]的质感与垂感。采用日落前明亮的黄金时刻光线与边缘光,背景为高度虚化的明亮车流与城市剪影。画面比例为3:4。赛博朋克潮流摄影,35mm镜头,f/1.4大光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止剪影化处理导致衣服发黑。【动作硬约束】模特上半身微倚天桥栏杆,肩部放松,双手自然垂放于身体两侧胯部附近,栏杆金属管避开衣服下摆高度,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特倚靠在城市明亮的天桥栏杆旁,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏暖光下的视觉效果。采用日落黄金时刻的明亮顺侧光,金色阳光直接铺满衣服正面而非逆光,色彩温暖且印花清晰锐利,背景为高度虚化的明亮车流与城市天际线。画面比例为3:4。潮流摄影,50mm镜头,f/1.4大光圈,8k分辨率。
- name: "旱冰场站姿场景"
- name: "旱冰场服装主体站姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特穿着旱冰鞋站在复古旱冰场中央,姿态从容,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。明亮的紫粉色调与射灯打在模特身上,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,35mm镜头,f/1.4大光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止场地彩光污染衣服原有色彩。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,姿态从容,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在复古旱冰场中央,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。场地采用明亮的主射灯独立照亮模特并配有正面柔光,衣服保持自身固有色不被环境粉紫光染色,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,50mm镜头,f/1.4大光圈,8k分辨率。
- name: "明亮现代地下车库站姿场景"
- name: "明亮现代地下车库服装主体站姿场景"
prompt: |
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,眼神冷酷,身体转回正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯照明,画面清透无暗角,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。赛博朋克潮流摄影,55mm镜头,f/2.0光圈,8k分辨率。
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔背靠水泥柱,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯正面补光组合,画面清透无暗角,衣服受光均匀对比适中,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。潮流摄影,85mm镜头,f/2.0光圈,8k分辨率。
BIN
View File
Binary file not shown.
+55 -14
View File
@@ -26,6 +26,7 @@ from graph.nodes import (
template_export_node,
)
from graph.state import AgentState
from graph.validate import with_fallback
def build_graph():
@@ -59,8 +60,16 @@ def build_graph():
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)
if target <= 0:
target = 1
@@ -72,11 +81,14 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
if max_rounds <= 0:
max_rounds = max(target * 2, 5)
if len(briefs) >= target:
print(f"[pinterest_route] 简报已达目标 {len(briefs)}/{target},结束")
return "done"
# 简报池还有待处理/在途简报 → 先让后台消化,不分析新图也不采集
if pipe is not None and hasattr(pipe, "pending_count"):
pending = pipe.pending_count()
if pending > 0:
# 日志由 pinterest_wait 节点进入时统一打印(阻塞等待消化,避免此处高频刷屏)
return "wait"
# 图池还有未消费图片 → 继续分析(不搜索
# 简报池空闲 → 检查图池还有未消费图片 → 分析(不管简报离目标差多少,先把这批用完
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 "")
@@ -85,24 +97,31 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
except Exception: # noqa: BLE001
unused = []
if unused:
print(f"[pinterest_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target}")
print(f"[pinterest_route] 简报池空闲,图池还有 {len(unused)} 张未消费图片,补分析"
f"(简报 {len(briefs)}/{target}")
return "analyze"
# 图池不足 → 搜索
# 简报池空闲 + 图池空 → 检查简报是否已达标
if len(briefs) >= target:
print(f"[pinterest_route] 图池已空,简报已达目标 {len(briefs)}/{target},结束")
return "done"
# 简报池空闲 + 图池空 + 简报不达标 → 新一轮搜索采集
if rounds >= max_rounds:
print(f"[pinterest_route] 已达最大轮次 {max_rounds},简报 {len(briefs)}/{target},按现有结果继续")
print(f"[pinterest_route] 已达最大轮次 {max_rounds}图池已空,简报 {len(briefs)}/{target},按现有结果继续")
return "done"
if not terms and rounds > 0:
print(f"[pinterest_route] 无可用搜索词,停止搜索(简报 {len(briefs)}/{target}")
print(f"[pinterest_route] 无可用搜索词,图池已空,停止搜索(简报 {len(briefs)}/{target}")
return "done"
print(f"[pinterest_route] 图池不足,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target}")
print(f"[pinterest_route] 简报池空闲,图池不足,简报未达标,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target}")
return "search"
def build_pinterest_graph():
"""Pinterest 参考模式图(按需搜索循环 + 简报池并发生成):
pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze
→ [pinterest_route] 简报不足 → 回到 pinterest_search;达标 → pinterest_finalize
→ [pinterest_route] 简报池还有在途 → wait(等待后台消化)→ 回到路由;
简报池空闲 + 图池有图 → analyze;图池空 + 简报不足 → search;达标 → pinterest_finalize
(排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export
"""
from graph.nodes import (
@@ -113,11 +132,27 @@ def build_pinterest_graph():
from graph.nodes.pinterest_finalize_node import pinterest_finalize_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.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_wait", _pinterest_wait)
builder.add_node("pinterest_finalize", pinterest_finalize_node)
builder.add_node("template_export", template_export_node)
@@ -126,10 +161,12 @@ def build_pinterest_graph():
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", # 图池不足 → 新一轮搜索
"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("template_export", END)
return builder.compile()
@@ -232,5 +269,9 @@ def run_pinterest_ref(
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1,
"pinterest_rounds": 0,
"pinterest_attempted": [],
"pinterest_images": {},
"pinterest_briefs": [],
"pinterest_login": {},
"pinterest_pipeline": None,
}
return compiled.invoke(state)
+69 -10
View File
@@ -10,6 +10,7 @@
所有请求跳过环境代理(NO_PROXY),适配用户挂 VPN 时直连国内/自建网关。
"""
import base64
import json
import time
from pathlib import Path
from typing import Optional
@@ -31,6 +32,17 @@ _FINAL_STATUS = ("succeeded", "completed", "done")
_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:
"""把大图压缩到 <max_bytes(默认 2MB)再上传:
- 尺寸过大先缩放(合成输入 1600x2200 内足够);
@@ -171,8 +183,12 @@ class OpenAIImageBackend(ImageBackend):
"n": 1,
"size": size or cfg.get("size", "1024x1024"),
"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:
data["seed"] = seed
# 提交重试:异步路径不稳定 → 失败重试同步提交(最多 3 次);
@@ -181,6 +197,20 @@ class OpenAIImageBackend(ImageBackend):
for attempt in range(3):
resp = requests.post(f"{base_url}/images/edits", headers=headers, files=files,
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:
body = resp.text or ""
if "content_policy" in body and attempt < 2:
@@ -193,18 +223,24 @@ class OpenAIImageBackend(ImageBackend):
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
try:
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path)
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
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}")
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "",
seed: Optional[int] = None) -> str:
"""纯文生图:生成白底纯印花设计稿(standalone pure print design)。
size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
background: 配置 compose.background="transparent" 时传 background 参数 → 透明背景 PNG
gpt-image-1/2 等模型支持;网关不支持该参数时会被忽略或由网关兜底)。"""
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。"""
cfg = self._cfg
api_key = cfg.get("api_key", "")
model = cfg.get("model", "gpt-image-1")
@@ -220,17 +256,32 @@ class OpenAIImageBackend(ImageBackend):
"size": size or cfg.get("size", "1024x1024"),
"model": model,
"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:
data["seed"] = seed
bg = str(cfg.get("background") or "").strip()
if bg:
data["background"] = bg # 如 "transparent"(透明背景 PNG
last_err: Optional[str] = None
for attempt in range(3):
resp = requests.post(f"{base_url}/images/generations", headers=headers, json=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:
body = resp.text or ""
if "content_policy" in body and attempt < 2:
@@ -243,7 +294,15 @@ class OpenAIImageBackend(ImageBackend):
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
try:
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path)
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
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}")
+3 -6
View File
@@ -180,14 +180,11 @@ class MockBackend:
paths = list(image_paths or [])
return [{
"topic": term,
"concept": f"(启发式兜底)围绕「{term}」做原创{art_style}风格印花",
"motif": motif,
"art_style": art_style,
"color_palette": palette,
"composition": composition,
"suitable_for_print": True,
"negative_prompt": negative,
"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 按图逐张产出简报,顺序一一对应)
"ref_images": [str(paths[i])] if i < len(paths) else [],
"source": "pinterest",
+83 -67
View File
@@ -318,31 +318,56 @@ def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
# —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)——
# image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接),
# 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD (print-on-demand) T-shirt design analyst.
You receive Pinterest reference images for one search term. For each image, extract the VISUAL CONCEPT
(style, mood, motif, color palette, composition) that makes it appealing, then produce an ORIGINAL
T-shirt print design brief that captures that VIBE WITHOUT copying the image.
PINTEREST_ANALYZE_SYSTEM_PROMPT = """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:
- NEVER copy the image, never reproduce the exact artwork, characters, logos, or any text from it.
- Extract only the abstract style/mood/motif concept as inspiration.
- Produce an original, flat, print-ready design brief (no garment, no model, no background scene).
- COPYRIGHT-SAFE: no brands, no logos, no characters, no celebrities, no real persons, no franchises.
- AVOID: politics, religion, hate, violence, sexual content, alcohol, national flags.
- motif: English, concrete central subject of the print (e.g. "a smiling cat with a fish", "geometric mountain layers").
- art_style: English visual technique (e.g. "clean flat vector", "retro screen print").
- color_palette: English colors (e.g. "sunset orange, cream, dusty blue").
- composition: English layout (e.g. "centered emblem with balanced negative space").
- concept: Chinese, one sentence describing the design idea.
- negative_prompt: what to avoid (real people, likeness, characters, logos, text).
- image_prompt: a COMPLETE, fluent English text-to-image prompt for generating the ORIGINAL flat print
design artwork (the print itself, NOT a garment photo). Describe the motif, art style, colors, layout
and mood in natural English, as a standalone print. Do NOT include garment / shirt / model / mannequin /
background-scene / watermark words. Do NOT mention any size or white-background suffix — a fixed
"small centered print on pure white" suffix will be appended automatically by the system.
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.
Return JSON with the field "designs" (array of objects with keys:
motif, art_style, color_palette, composition, concept, negative_prompt, image_prompt)."""
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 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.
negative_prompt: copy of reference artwork, likenesses, characters, logos,
trademarks, watermark, photorealistic shirt/product mockups, busy background;
add garbled-lettering terms only if your design includes text.
OUTPUT — ONLY valid JSON, no fences:
{"designs":[{"suitable_for_print":<bool>,"negative_prompt":"<str>","image_prompt":"<str>"}]}"""
PINTEREST_ANALYZE_SCHEMA = {
"name": "pinterest_design_briefs",
@@ -354,17 +379,11 @@ PINTEREST_ANALYZE_SCHEMA = {
"items": {
"type": "object",
"properties": {
"image_index": {"type": "integer"},
"motif": {"type": "string"},
"art_style": {"type": "string"},
"color_palette": {"type": "string"},
"composition": {"type": "string"},
"concept": {"type": "string"},
"suitable_for_print": {"type": "boolean"},
"negative_prompt": {"type": "string"},
"image_prompt": {"type": "string"},
},
"required": ["image_index", "motif", "art_style", "color_palette",
"composition", "concept", "negative_prompt", "image_prompt"],
"required": ["suitable_for_print", "negative_prompt", "image_prompt"],
"additionalProperties": False,
},
}
@@ -375,18 +394,10 @@ PINTEREST_ANALYZE_SCHEMA = {
}
def build_pinterest_analyze_user_prompt(term: str, country: str, image_count: int) -> str:
def build_pinterest_analyze_user_prompt() -> str:
return (
f"Country: {country}\n"
f"Pinterest search term: {term}\n"
f"Reference images attached: {image_count} images.\n\n"
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 "
f"it was derived from (first image = 0, second = 1, ...). Every image_index from 0 to "
f"{max(image_count - 1, 0)} must appear exactly once — this links each brief to its "
f"source image so the design is generated from the SAME image that was analyzed."
"Analyze the attached image and produce one ORIGINAL T-shirt print design brief "
"that captures its visual vibe without copying it."
)
@@ -587,8 +598,7 @@ class OpenAICompatBackend(LLMBackend):
on_400=None) -> List[Dict[str, Any]]:
"""多模态分析 Pinterest 图片 → 原创设计简报列表。
图片输入不被模型支持(纯文本模型 400)时自动降级纯文本分析(仅用搜索词)
失败返回 [],由节点兜底(回退 mock 规则简报)。
图片输入失败/无有效图片时直接放弃(返回 [],不降级纯文本),由节点跳过该产品
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
"""
cfg = self._cfg
@@ -605,11 +615,26 @@ class OpenAICompatBackend(LLMBackend):
data_uris: List[str] = []
for p in image_paths:
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
mime = "image/png"
if Path(p).suffix.lower() in (".jpg", ".jpeg"):
if pk.suffix.lower() in (".jpg", ".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
print(f"[pinterest_analyze] 图片读取失败 {p}: {e}")
@@ -623,12 +648,11 @@ class OpenAICompatBackend(LLMBackend):
except Exception: # noqa: BLE001
pass
def _call(use_images: bool) -> str:
def _call() -> str:
user_content: List[Any] = [
{"type": "text", "text": build_pinterest_analyze_user_prompt(term, country, len(data_uris))},
{"type": "text", "text": build_pinterest_analyze_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 = {
"model": model,
"messages": [
@@ -656,19 +680,15 @@ class OpenAICompatBackend(LLMBackend):
resp.raise_for_status()
return str(resp.json()["choices"][0]["message"].get("content") or "")
raw = ""
if data_uris:
try:
raw = _call(use_images=True)
except Exception as e: # noqa: BLE001 纯文本模型不支持图片 → 降级纯文本
print(f"[pinterest_analyze] 图片输入失败,降级纯文本分析: {e}")
raw = ""
if not raw:
try:
raw = _call(use_images=False)
except Exception as e: # noqa: BLE001
print(f"[pinterest_analyze] 分析失败: {e}")
return []
# 图片输入失败/无有效图片 → 直接放弃该产品(不降级纯文本),由节点跳过后续流程
if not data_uris:
print("[pinterest_analyze] 无有效图片输入,放弃该产品(不降级纯文本)")
return []
try:
raw = _call()
except Exception as e: # noqa: BLE001
print(f"[pinterest_analyze] 图片分析失败,放弃该产品(不降级纯文本): {e}")
return []
try:
parsed = _extract_json(raw)
except Exception as e: # noqa: BLE001
@@ -687,11 +707,7 @@ class OpenAICompatBackend(LLMBackend):
continue
designs.append({
"topic": term,
"concept": str(d.get("concept", "")).strip(),
"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(),
"suitable_for_print": bool(d.get("suitable_for_print", True)),
"negative_prompt": str(d.get("negative_prompt", "")).strip(),
"image_prompt": str(d.get("image_prompt", "")).strip(),
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)
+22 -2
View File
@@ -12,7 +12,7 @@ import time
from pathlib import Path
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": "⛔ 拦截"}
@@ -29,6 +29,18 @@ def _notify_400(on_400, exc) -> None:
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:
lines = [
"# 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,
seed: Optional[int] = None,
on_400=None,
on_503=None,
size: str = "1024x1024") -> Optional[str]:
"""生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。
@@ -123,6 +136,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
on_503: 致命图像服务错误(503/账户不可用)时回调(供调用方提前终止任务)。
"""
try:
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
print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}")
_notify_400(on_400, e)
_notify_503(on_503, e)
out_path = ib.generate(
img_prompt, str(design_dir / f"{out_stem}_design.png"),
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
except Exception as e: # noqa: BLE001
_notify_400(on_400, e)
_notify_503(on_503, e)
if errors is not None:
errors.append({"node": "compose", "type": type(e).__name__,
"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]):
"""单张设计稿生成(并发线程内调用,每设计一线程)。"""
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"))
if out_path is None:
return i, b, None, None
return i, b, out_path, None
_safe_errors = ThreadSafeErrors()
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))))
@@ -263,6 +280,9 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
b["design_path"] = out_path
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": 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["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)}
+4 -4
View File
@@ -7,6 +7,8 @@
1. 先用种子词走数据源抓取(Google Trends 等),成功即用新数据;
2. 抓取失败/无结果才回退 output/<国>/collected_keywords.json 旧缓存,保证流水线不中断。
"""
import json
from pathlib import Path
from typing import Any, Dict, List
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)
if use_collected:
try:
import json as _json
from pathlib import Path as _Path
p = _Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
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 []
if cached_rows:
rows = [dict(r) for r in cached_rows] # 已过滤去重的关键词
+7 -7
View File
@@ -5,6 +5,9 @@
2) 真实人物(名单 + Firstname Lastname 模式,仅对 gt_trending 源,避免误删风格词)
3) 设计相关性(剔除泛新闻/科技/赛事词)
"""
import json
import time
from pathlib import Path
from typing import Any, Dict, List
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 仅含本次限量生成的热点)。
if kept:
try:
import json as _json
from pathlib import Path as _Path
p = _Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
p.parent.mkdir(parents=True, exist_ok=True)
kws = [{"topic": r.get("topic", ""), "source": r.get("source", ""),
"kind": r.get("kind", ""), "raw_score": r.get("raw_score")} for r in kept]
p.write_text(_json.dumps({"country": country,
"collected_at": _datetime_now(),
"keywords": kws}, ensure_ascii=False, indent=2),
p.write_text(json.dumps({"country": country,
"collected_at": _datetime_now(),
"keywords": kws}, ensure_ascii=False, indent=2),
encoding="utf-8")
print(f"[filter] 已写入采集缓存 {len(kws)} 条(collected_keywords.json")
except Exception as e: # noqa: BLE001
@@ -88,5 +89,4 @@ def filter_node(state: Dict[str, Any]) -> Dict[str, Any]:
def _datetime_now() -> str:
import time
return time.strftime("%Y-%m-%dT%H:%M:%S")
+46 -77
View File
@@ -3,16 +3,16 @@
图池机制:
- 从持久化图池(image_pool.json)取「未消费」图片(md5 不在 used_images.json)。
- 大图先压缩(内存占用过大 → 缩放/重编码)再送 LLM。
- 多并发分析(每批 analyze_per_term 张,并发 analyze_concurrency 线程)。
- 多并发分析(每批 1 张,并发 analyze_concurrency 线程;一次 API 请求一张图,利于 LLM 注意力)。
- 每张被分析的图片 md5 一律拉黑(used_images.json)——合适→产出简报→生成设计(设计 md5 全局拉黑见 compose);
不合适→图片 md5 已拉黑→下一轮自动取下一张,不重复分析。
- 图池无未消费图片时返回空,由路由触发新一轮搜索。
兜底链:LLM 多模态 → 纯文本降级(后端内部)→ mock 规则简报 → 空列表(下游跳过)。
兜底链:LLM 多模态分析失败/无图直接放弃本轮(不降级纯文本、不做 mock 兜底)。
带 with_fallback:任何异常都不中断。
"""
import concurrent.futures
import re
import uuid
from pathlib import Path
from typing import Any, Dict, List
@@ -25,25 +25,14 @@ from graph.pinterest import (
pool_unused_images,
save_used_images,
)
from graph.validate import 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,
)
from graph.validate import ThreadSafeErrors, with_fallback
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":
return False
motif = str(b.get("motif") or "").strip()
if not motif:
return False
if _BRIEF_UNSUITABLE.search(motif):
if not bool(b.get("suitable_for_print", True)):
return False
return True
@@ -75,25 +64,21 @@ def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str,
n = seen_topics.get(base.lower(), 0)
seen_topics[base.lower()] = n + 1
topic = base if n == 0 else f"{base} #{n + 1}"
motif = str(b.get("motif") or "").strip() or term
if not motif:
continue
if not bool(b.get("suitable_for_print", True)):
continue # LLM 判定不适合做印花 → 丢弃(md5 已拉黑,下轮取新图)
out.append({
"country": country,
"topic": topic,
"risk_level": str(b.get("risk_level") or "safe").strip().lower() or "safe",
"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),
"concept": str(b.get("concept") or "").strip() or 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(),
"concept": f"围绕「{term}」的原创印花设计",
"negative_prompt": str(b.get("negative_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)],
"source_md5": str(b.get("source_md5") or "").strip().lower(),
"brief_id": str(b.get("brief_id") or "").strip(),
"slogan": "",
"score": 1.0,
"confidence": 1.0,
@@ -110,23 +95,25 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
errors = list(state.get("errors") 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))
max_designs = int(pcfg.get("max_designs", 10))
n_ref = max(1, int(pcfg.get("ref_images_per_design", 1)))
provider = str(pcfg.get("provider") or "openai").strip().lower()
# 按需分析:只取补齐到目标所需的图片数(batch_size 为上限,不超额分析),并按 md5 去重,
# 保证同一图片内容(md5)不会同时被多条简报使用
target = int(state.get("pinterest_target") or 0)
existing = state.get("briefs") or []
remaining = max(0, target - len(existing))
# 简报池还有待处理/在途简报 → 不分析新图(等后台消化完再由路由决定下一步)
pipe = state.get("pinterest_pipeline")
if pipe is not None and hasattr(pipe, "pending_count") and pipe.pending_count() > 0:
print(f"[pinterest_analyze] 简报池还有 {pipe.pending_count()} 条在途,本轮不分析")
return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
"errors": errors}
# 按需分析:一次把图池当前未消费图片全部分析成简报(搜集完一批→生成一批简报),
# 不再按目标任务数截断(不够用了才由路由触发新采集)。analyze_batch 可设上限保护配额。
batch_size = int(pcfg.get("analyze_batch", 0))
if batch_size <= 0:
# 自动:一次分析补齐到「目标所需」或「每词简报上限」的较小值(每张图→1条简报)
# 让 pipeline 队列一次有足够任务,时刻保持并发生成(避免每轮只推 6 条导致线程空转)
batch_size = min(remaining, max_designs)
need = min(batch_size, remaining) if remaining > 0 else 0
batch_size = max_designs # 自动:一次最多分析 max_designs 张(每张图→1条简报)
need = batch_size
# 1) 图池取未消费图片(md5 不在 used_images);无 → 返回空,路由触发搜索
pool = load_image_pool(output_dir, country)
@@ -137,10 +124,6 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
"errors": errors}
if need <= 0:
print("[pinterest_analyze] 简报已达标,无需分析")
return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
"errors": errors}
seen_md5: set = set()
batch: List[Dict[str, Any]] = []
for img in unused:
@@ -148,34 +131,27 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
if m and m in seen_md5:
continue # 同一图片内容(md5)不重复分析
seen_md5.add(m)
img["brief_id"] = str(uuid.uuid4()) # md5 校验后分配全局唯一 id,用于图-简报对应校验
batch.append(img)
if len(batch) >= need:
break
print(f"[pinterest_analyze] 图池取 {len(batch)} 张未消费图片分析(按需 {need}"
print(f"[pinterest_analyze] 图池取 {len(batch)} 张未消费图片分析(本批上限 {need}"
f"池剩余未消费 {len(unused) - len(batch)} 张,已消费 {len(used)} 张)")
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);
无 image_indexmock 兜底)→ 回退按顺序;无效/越界/重复 → 丢弃该简报(避免错位)。
这样 analyze_per_term 可 >1 一次分析多张图提速,简报仍严格对应各自的图。
全局 id 校验:分析前已给每张图分配 uuid4(brief_id),简报回填同一 brief_id
保证图-简报严格对应(一次 API 请求一张图,无 image_index 错位问题)。
"""
chunk_paths = [img["path"] 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]] = []
for i, b in enumerate(res):
if not isinstance(b, dict):
continue
try:
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)
idx = i if i < len(chunk_paths) else 0 # 一次一张:简报按顺序对应唯一图
refs: List[str] = []
for k in range(n_ref):
src = chunk_paths[(idx + k) % len(chunk_paths)]
@@ -183,6 +159,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
refs.append(src)
b["ref_images"] = refs
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)
return out
@@ -210,6 +187,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
# 4) 多并发分析(每线程分析一个 chunk;LLM 后端只读 self._cfg,线程安全)
raw_briefs: List[Dict[str, Any]] = []
_safe_errors = ThreadSafeErrors()
def _analyze_chunk(chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
term = str(chunk[0].get("term") or "")
@@ -225,33 +203,27 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
# 把每条简报的来源图路径回填为原始图(压缩图仅用于分析,参考图用原图)
return _assign_refs(res, chunk)
except Exception as e: # noqa: BLE001
errors.append({"node": "pinterest_analyze", "type": type(e).__name__,
"message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""})
_safe_errors.append({"node": "pinterest_analyze", "type": type(e).__name__,
"message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""})
print(f"[pinterest_analyze] 「{term}」分析失败: {e}")
return []
return []
workers = max(1, min(concurrency, len(chunks)))
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:
_futs = [_ex.submit(_analyze_chunk, c) for c in chunks]
for _f in concurrent.futures.as_completed(_futs):
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:
try:
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}")
print("[pinterest_analyze] 图片分析无结果,放弃本轮简报(跳过后续流程)")
# 6) 本批所有图片 md5 一律拉黑(已消费,不再复用)——合适/不合适都拉黑
for img in batch:
@@ -267,19 +239,20 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
if len(kept) < len(raw_briefs):
print(f"[pinterest_analyze] 简报过滤:{len(raw_briefs)}{len(kept)} 条适合印花")
# 8) 上限 + 去重(同 motif+style 指纹只留一条
# 8) 上限 + 去重(同 brief_id 只留一条;brief_id 为每张图 uuid4,天然唯一
kept = kept[:max_designs]
seen: set = set()
uniq: List[Dict[str, Any]] = []
for b in kept:
fp = f"{str(b.get('motif', '')).strip().lower()}|{str(b.get('art_style', '')).strip().lower()}"
if fp in seen:
fp = str(b.get("brief_id") or b.get("image_prompt") or "").strip().lower()
if not fp or fp in seen:
continue
seen.add(fp)
uniq.append(b)
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 [])]
screened = _enrich_briefs(kept, country, existing_topics)
new_briefs: List[Dict[str, Any]] = []
@@ -290,10 +263,6 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
old_count = len(state.get("briefs") or [])
accumulated = list(state.get("briefs") or [])
accumulated.extend(new_briefs)
target = int(state.get("pinterest_target") or 0)
if target > 0 and len(accumulated) > target:
accumulated = accumulated[:target]
print(f"[pinterest_analyze] 简报已达目标 {target} 条,截断多余部分")
# 推送实际新增且保留的简报到并发生成流水线(简报池):边分析边生成设计/三合一/种草图
pushed = accumulated[old_count:]
+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))
proxy = pcfg.get("proxy") or None
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 是单例,
# 并发启动会互相抢占导致 "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}")
concurrency = 1
# 一次性探测并校验代理(Pinterest 需代理才能访问;代理失效时给出明确警告,避免逐词静默失败)
# 默认走代理:config.pinterest.proxy 未配置时自动探测(环境变量/系统代理/本地常见端口),
# 本地 VPN 已开启时探测到的代理即可访问 Pinterest;代理失效时给出明确警告,避免逐词静默失败。
if proxy is None:
try:
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:
from pinterest_scraper.scraper import scrape_pinterest
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
except Exception as e: # noqa: BLE001
failed.append(term)
@@ -150,8 +177,9 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
stats["pinterest_scrape"] = {
"terms": len(terms), "scraped": len(results), "skipped": len(skipped),
"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)}")
return {"pinterest_images": results, "pinterest_attempted": attempted,
"stats": stats, "errors": errors}
"pinterest_login": login_state, "stats": stats, "errors": errors}
+103 -47
View File
@@ -34,22 +34,18 @@ from typing import Any, Dict, List, Optional
from graph.paths import project_root, runtime_root
from graph.product import (
find_basemap,
find_first_model_folder,
first_available_sku,
list_colors,
list_spus,
)
from graph.validate import with_fallback
from graph.validate import ThreadSafeErrors, with_fallback
_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:
"""货号续号:扫 prod_dir 已有 {prefix}{数字}* 文件,返回下一个起始序号(不覆盖旧产物)。"""
import re
max_n = -1
try:
if prod_dir.exists():
@@ -116,17 +112,30 @@ def _template_out_path(prod_dir: Path, chosen_sku: str) -> Path:
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):
"""图像合成带退避重试(网关超载/超时常见):成功返回 out_path;全部失败返回 None。"""
import time as _t
"""图像合成带退避重试(网关超载/超时常见):成功返回 out_path;全部失败返回 None。
致命 503(账户不可用)重试无效 → 直接抛出,交由调用方终止任务。
"""
last = None
for i in range(attempts):
try:
return fn(*args, **kwargs)
except Exception as e: # noqa: BLE001
if _is_fatal_50x(e):
raise
last = e
if i < attempts - 1:
_t.sleep(backoff[i])
time.sleep(backoff[i])
print(f"[product] 图像合成重试 {attempts} 次均失败: {last}")
return None
@@ -135,16 +144,31 @@ def _process_spu(
db_path, basemap_root, material_root, category, prod_dir, brief, ib,
spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="",
img_code="", model_img=None, design_size="1024x1024", compose_size="1536x2048",
on_503=None,
) -> Optional[Dict[str, Any]]:
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
img_code: 货号(前缀+3位计数);本产品所有图片文件归入 prod_dir/{img_code}/ 子文件夹
(按货号命名,包含该货号对应的所有图片)。
on_503: 致命图像服务错误(503/账户不可用)回调(供调用方提前终止任务)。
返回 result dict;内部异常已兜底,不中断。
"""
# 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里
prod_dir = prod_dir / img_code
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"])
valid_codes = {c["sku_code"] for c in colors}
if sku_code:
@@ -220,6 +244,9 @@ def _process_spu(
result["design_from"] = "product"
print(f"{tag} 纯印花设计稿已生成(product 节点): {design_path}")
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": ""})
print(f"{tag} 设计稿生成失败: {e}")
@@ -262,17 +289,26 @@ def _process_spu(
print(f"{tag} 三图模特合成图已生成(耗时 {int(time.time()-t0)}s: {composite_path}")
except Exception as e: # noqa: BLE001
# 合成失败 → 带退避重试(网关超载/超时常见,重试 3 次)
print(f"{tag} 三图合成失败,退避重试…: {e}")
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}")
if _fatal(e):
print(f"{tag} 图像服务 503,终止: {e}")
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:
# mark=1 无模特图 → 统一只做三合一,不做印花+底图两图合成
if int(spu.get("mark") or 0) == 1:
@@ -289,17 +325,26 @@ def _process_spu(
result["printed_path"] = printed_path
print(f"{tag} 平铺服装图已生成(无模特,底图+印花): {printed_path}")
except Exception as e: # noqa: BLE001
print(f"{tag} 平铺服装图失败,退避重试…: {e}")
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}")
if _fatal(e):
print(f"{tag} 图像服务 503,终止: {e}")
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 多色时每个颜色再执行一次三合一(用各自底图),轮播图按颜色路由
color_composites: List[Dict[str, Any]] = []
@@ -322,6 +367,9 @@ def _process_spu(
color_composites.append({"sku_code": sc, "color": col, "composite_path": cp})
print(f"{tag} 颜色 {sc}{col})三合一已生成: {cp}")
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__,
"message": f"颜色 {sc} 三合一失败: {e}", "trace": ""})
result["color_composites"] = color_composites
@@ -445,23 +493,25 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
prod_dir = output_dir / "product"
prod_dir.mkdir(parents=True, exist_ok=True)
# 4.1) 任务级模特分配(material_library-<category>):
# 一个 SPU 对应一个模特;SPU(不同款)数 > 模特数 → 从全部模特循环兜底(允许重复
model_assign: Dict[str, Any] = {}
_all_models: List[str] = []
# 4.1) 任务级模特分配(按 spu.mark 映射模特目录 → 过滤非 3:4 图片 → 每个任务随机抽取):
# 每个产品任务(含同一 SPU 的多个款)都独立随机抽一个模特,保证同款多产品模特不重复
model_assign: Dict[int, Any] = {}
try:
_folder, _all_models = find_first_model_folder(material_root, category)
from graph.product import build_mark_model_map, find_model_images_for_mark
mark_map = build_mark_model_map(db_path, material_root)
except Exception: # noqa: BLE001
_all_models = []
if _all_models:
seen_spu: Dict[str, str] = {}
for _i, (_spu, _skus, _tb) in enumerate(worklist):
code = _spu.get("code", "")
if code not in seen_spu:
seen_spu[code] = _all_models[_i % len(_all_models)] # SPU>模特数 → 循环兜底
model_assign[code] = seen_spu[code]
print(f"[product] 任务级模特分配:{len(seen_spu)} 个 SPU,模特池 {len(_all_models)}"
f"{'SPU>模特,循环兜底)' if len(seen_spu) > len(_all_models) else ''}")
mark_map = {}
for _i, (_spu, _skus, _tb) in enumerate(worklist):
mark = str(_spu.get("mark") or "").strip() or "1"
folder = mark_map.get(mark, category)
try:
pool_imgs = find_model_images_for_mark(db_path, material_root, mark, folder)
except Exception: # noqa: BLE001
pool_imgs = []
if pool_imgs:
model_assign[_i] = random.choice(pool_imgs) # 过滤后随机抽(按任务序号)
if model_assign:
print(f"[product] 任务级模特分配:{len(model_assign)} 个产品任务(按 mark 过滤 3:4 后随机抽取)")
# 5) compose 节点生成的共享设计稿(图2):每个任务用自己的热点简报设计(tb.design_path),
# 一个货号对应一个设计(多颜色共用该设计),不再所有产品共用第一个
@@ -503,8 +553,11 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
concurrency = int(pcfg.get("concurrency") or 0) or min(len(worklist), 5)
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 内部兜底。"""
idx = start_idx + wi # 实际货号序号(start_idx 起自动续号)
img_code = f"{prefix}{idx:03d}" # 货号:图片按此命名(DG000_design.png…)
try:
# 每个任务用自己的热点设计(designs_map),并拷贝为货号命名(designs/DG000_design.png
@@ -519,9 +572,9 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
tb = dict(tb)
tb["design_path"] = design_path
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,
model_img=model_assign.get(spu.get("code", "")),
model_img=model_assign.get(wi), # 按任务序号取独立随机模特
design_size=str((config.get("compose") or {}).get("design_size") or "1024x1024"),
compose_size=str((config.get("compose") or {}).get("size") or "1536x2048"))
if r:
@@ -535,7 +588,7 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
# 货号自动续号:任务一开始全部按序分配(start_idx 起),不覆盖已生成的产物
start_idx = _next_img_idx(prod_dir, prefix)
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 f in concurrent.futures.as_completed(futures):
r, img_code = f.result()
@@ -545,6 +598,9 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
_record_used(cache_dir, r) # (热点-风格) 去重记录 → 缓存根目录
except Exception: # noqa: BLE001
pass
# 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加)
if len(_safe_errors):
errors.extend(list(_safe_errors))
results.sort(key=lambda x: x.get("img_code", "")) # 按货号排序,模板/清单顺序稳定
_write_products(prod_dir, results)
+44 -18
View File
@@ -22,6 +22,27 @@ PINTEREST_PRINT_SUFFIX = (
"no garment, no shirt, no model, no mannequin, no watermark"
)
# Pinterest 生图提示词 4 段结构中第 3 段的引导前缀:把 LLM 产出的 negative_prompt
# 转成一条正向「Strictly avoid: ...」条款拼进 image_prompt,让防复制/防商标约束落到生成指令
NEG_LEAD = "Strictly avoid: "
# Pinterest 生图提示词 4 段结构中第 4 段(仅当设计含文字时追加):
# 要求模型把引号内的文字按原文逐字正确拼写,避免乱码/拼错
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."
)
# 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif,
# 避免 gpt-image 等内容策略频繁拦截导致"生图限制多")
_IMG_RISKY_SWAP = {
@@ -73,27 +94,32 @@ def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]:
composition = (r.get("composition") or derive_composition(r["topic"], r.get("design_category"))).strip()
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_neg = (r.get("negative_prompt") or "").strip()
if r.get("source") == "pinterest" and llm_ip:
prompts["image_prompt"] = llm_ip + PINTEREST_PRINT_SUFFIX
print(f"[prompt] Pinterest 简报用 LLM 多模态描述作为 image_prompt(跳过四要素模板): 「{r['topic']}")
# 文字印花(约 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(疑似商标/受保护主题)→ 动态注入「原创化魔改」引导:只做风格参考,禁止复刻品牌/商标/角色,
# 换名换细节,生成通用非侵权的致敬式设计
# —— Pinterest 参考模式:跳过四要素模板,按 4 段结构拼 image_prompt ——
# ① image_prompt(分析模型产出) + 固定输出形态后缀 PINTEREST_PRINT_SUFFIX
# ② 负向条款(由 LLM negative_prompt 经 NEG_LEAD 引导,转化进正向指令)
# ③ 拼写锁定句 SPELLING_RULE(仅当 LLM image_prompt 已含引号文字段时)
# 是否含文字、拼写与否均由分析模型产出决定,本模式不注入 slogan。
seg: List[str] = [llm_ip, PINTEREST_PRINT_SUFFIX.strip()]
if llm_neg:
seg.append(NEG_LEAD + llm_neg)
if '"' in llm_ip:
seg.append(SPELLING_RULE)
prompts["image_prompt"] = ", ".join(seg)
print(f"[prompt] Pinterest 简报按 4 段结构拼 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":
prompts["image_prompt"] = (prompts["image_prompt"]
+ " 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.")
prompts["image_prompt"] = prompts["image_prompt"] + " " + REVIEW_REBRAND_HINT
print(f"[prompt] review 简报注入原创化魔改引导: 「{r['topic']}")
r.update(prompts)
r["motif"] = motif
+15 -9
View File
@@ -20,14 +20,8 @@ from graph.validate import with_fallback
def _template_out_path(prod_dir: Path, tpl_name: str) -> Path:
"""模板输出路径:默认 {tpl_name}_已填写.xlsx;已存在/被占用则自动换名加序号(同款号多产品不互相覆盖)。"""
base = prod_dir / f"{tpl_name}_已填写.xlsx"
try:
with open(base, "ab"):
pass
except OSError:
pass
else:
if not base.exists():
return base
if not base.exists():
return base
for i in range(2, 100):
cand = prod_dir / f"{tpl_name}_已填写_{i}.xlsx"
if not cand.exists():
@@ -95,6 +89,7 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
sku_codes = [r.get("sku_code") or ""]
batch.append({
"spu_code": r.get("spu_code", ""),
"img_code": r.get("img_code", ""),
"sku_codes": sku_codes,
"images": [],
"spu_per_color": True, # 每颜色一个独立 SPU 块(单色多 SPU)
@@ -107,13 +102,24 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
"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] = []
if batch:
out = _template_out_path(prod_dir, "商品上传")
# 输出文件名 = 模板原文件名 + _已填写(如 NEW-波兰男黑T恤_已填写.xlsx
out = _template_out_path(prod_dir, Path(tp).stem)
try:
out = export_products(
db_path, batch, tdir, tp, str(out),
markup_percent=float(pcfg.get("markup_percent") or 0),
suggested_price_ratio=float(pcfg.get("suggested_price_ratio") or 0),
)
for r in products:
if (r.get("composite_path") or r.get("printed_path")) and (r.get("en_title") or "").strip():
-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)。"""
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}"
def random_code4() -> str:
"""4 位随机:大小写英文 + 数字。"""
import random
import string
return "".join(random.choices(string.ascii_letters + string.digits, k=4))
+190 -22
View File
@@ -12,6 +12,7 @@
并发上限与 product_node 一致(默认 5),避免压垮图像网关。
"""
import concurrent.futures
import random
import re
import threading
import time
@@ -35,13 +36,23 @@ class PinterestPipeline:
self._briefs: List[Dict[str, Any]] = []
self._done = False
self._cursor = 0
# 正在处理(已提交线程池、尚未完成)的简报数,供路由判断简报池是否空闲
self._in_flight = 0
# 结果
# 结果:每完成一个产品立即落盘追加写入 products_pending.jsonl
# 内存列表仅作缓存(finish 时再读盘合并),中途崩溃也不丢已完成产品。
self._products: List[Dict[str, Any]] = []
self._products_lock = threading.Lock()
self._errors: List[Dict[str, Any]] = []
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 逻辑:运行根优先,其次数据根)
pcfg = self.config.get("product") or {}
@@ -188,19 +199,28 @@ class PinterestPipeline:
return None
def _assign_models(self) -> Dict[str, Any]:
"""任务级模特分配:按 spu.mark 映射模特目录 → 过滤非 3:4 图片 → 每个任务随机抽取。
每个产品任务(含同一 SPU 的多个款)都独立随机抽一个模特,保证同款多产品模特不重复。
"""
model_assign: Dict[str, Any] = {}
try:
from graph.product import find_first_model_folder
_folder, _all_models = find_first_model_folder(self._material_root, self._category)
from graph.product import build_mark_model_map, find_model_images_for_mark
mark_map = build_mark_model_map(self._db_path, self._material_root)
except Exception: # noqa: BLE001
_all_models = []
if _all_models:
seen: Dict[str, str] = {}
for _i, (spu, _skus) in enumerate(self._worklist):
code = spu.get("code", "")
if code not in seen:
seen[code] = _all_models[_i % len(_all_models)]
model_assign[code] = seen[code]
mark_map = {}
# 每个任务按序号绑定独立随机模特(同 SPU 多款也各自随机,不共用)
for _i, (spu, _skus) in enumerate(self._worklist):
key = f"task_{_i}"
mark = str(spu.get("mark") or "").strip() or "1"
folder = mark_map.get(mark, self._category)
try:
pool_imgs = find_model_images_for_mark(self._db_path, self._material_root,
mark, folder)
except Exception: # noqa: BLE001
pool_imgs = []
if pool_imgs:
model_assign[key] = random.choice(pool_imgs) # 过滤后随机抽
return model_assign
def _load_materials(self) -> Dict[str, str]:
@@ -237,6 +257,48 @@ class PinterestPipeline:
with self._err400_lock:
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:
"""放弃当前种子词:清空其未完成简报 + 图池未消费图片(已完成的保留)。"""
term = self._err400_term
@@ -298,18 +360,52 @@ class PinterestPipeline:
self._cond.notify_all()
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 finish(self) -> tuple:
"""排空简报池、等待全部产品完成,返回 (products, errors)。"""
"""排空简报池、等待全部产品完成,返回 (products, errors)。
产品来源:手动已完成(内存缓存)+ 落盘文件(products_pending.jsonl
按货号去重合并——即使中途 503 终止/崩溃,已完成产品也不丢。
"""
with self._cond:
self._done = True
self._cond.notify_all()
self._dispatcher.join()
self._pool.shutdown(wait=True)
# 读盘 + 内存合并去重(内存为准,但以落盘为最终权威——崩溃恢复后走落盘)
pending = self.load_pending()
merged = {str(p.get("img_code", "")): p for p in pending}
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:
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
# ------------------------------------------------------------------ #
@@ -322,8 +418,16 @@ class PinterestPipeline:
self._cond.wait()
if self._done and not self._briefs:
break
if self._fatal_503:
# 致命 503:不再分发新简报(未完成的废弃,仅保留已完成落盘产品)
self._briefs = []
self._done = True
self._cond.notify_all()
break
batch = self._briefs
self._briefs = []
# 与 _briefs 清空同一临界区递增在途数,避免 wait_idle 误判空闲
self._in_flight += len(batch)
for b in batch:
with self._cond:
idx = self._cursor
@@ -335,6 +439,8 @@ class PinterestPipeline:
# ------------------------------------------------------------------ #
def _process_one(self, brief: Dict[str, Any], idx: int) -> None:
try:
if self.is_fatal_503_aborted():
return
# 0) 先定货号:整条链路(设计/三合一/种草图)都用它命名与匹配,避免序号错位
if idx >= len(self._worklist):
print(f"[pinterest_pipeline] 简报 {idx} 无对应产品任务,跳过")
@@ -343,6 +449,8 @@ class PinterestPipeline:
img_code = f"{self._prefix}{idx:03d}"
# 1) 生成设计(compose)——直接按货号命名 designs/{img_code}_design.png
design_path = self._gen_design(brief, img_code)
if self.is_fatal_503_aborted():
return
if self.is_400_aborted():
# 当前种子词 400 超限已放弃:正在生成的当个也放弃,不进入后续链路
print(f"[pinterest_pipeline] 当前种子词 400 超限已放弃,跳过简报 {idx}")
@@ -357,13 +465,17 @@ class PinterestPipeline:
return
brief = new_brief
design_path = self._gen_design(brief, img_code)
if self.is_fatal_503_aborted():
return
if design_path:
break
if not design_path:
return
brief["design_path"] = design_path
# 2) 三合一(product)——同一货号
prod = self._process_spu(brief, spu, skus, img_code, design_path)
# 2) 三合一(product)——同一货号;task_idx=本次简报序号,模特按任务独立随机
prod = self._process_spu(brief, spu, skus, img_code, design_path, task_idx=idx)
if self.is_fatal_503_aborted():
return
if not prod:
return
# 3) OSS 上传
@@ -376,6 +488,8 @@ class PinterestPipeline:
_record_used(self.cache_dir, prod)
except Exception: # noqa: BLE001
pass
# 5) 落盘:每完成一个产品立即追加写入 products_pending.jsonl(不依赖内存,崩溃不丢)
self._persist_product(prod)
with self._products_lock:
self._products.append(prod)
print(f"[pinterest_pipeline] 产品完成: {prod.get('img_code', '')}"
@@ -385,6 +499,42 @@ class PinterestPipeline:
self._errors.append({"node": "pinterest_pipeline", "type": type(e).__name__,
"message": f"简报 {idx} 处理失败: {e}", "trace": ""})
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 每行一个产品)。
落盘失败不阻塞主流程(仅告警);finish() 时读盘合并,保证已完成产品不丢。
"""
try:
import json as _json
self._pending_file.parent.mkdir(parents=True, exist_ok=True)
with open(self._pending_file, "a", encoding="utf-8") as f:
f.write(_json.dumps(prod, ensure_ascii=False) + "\n")
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]:
if self._ib is None:
@@ -398,10 +548,18 @@ class PinterestPipeline:
if self.record_400():
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,
on_400=_on_400,
on_400=_on_400, on_503=_on_503,
size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024"))
except Exception as e: # noqa: BLE001
if self.is_fatal_503(e):
self.record_503()
self.abort_unfinished()
return None
with self._errors_lock:
self._errors.append({"node": "compose", "type": type(e).__name__,
"message": f"设计稿生成失败 {brief.get('topic', '')}: {e}",
@@ -473,7 +631,7 @@ class PinterestPipeline:
return None
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
prod_dir = self.output_dir / "product"
prod_dir.mkdir(parents=True, exist_ok=True)
@@ -483,7 +641,9 @@ class PinterestPipeline:
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 {},
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,
model_img=self._model_assign.get(f"task_{task_idx}"),
on_503=lambda: (self.record_503(), self.abort_unfinished()))
if r:
r["img_code"] = img_code
return r
@@ -559,9 +719,17 @@ class PinterestPipeline:
if not base or not Path(base).exists():
print(f"[pinterest_pipeline] {r.get('spu_code', '')} 参考图缺失,跳过该色种草图")
continue
generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir),
r.get("composite_negative", ""),
size=size, prefix=pfx, gender=self._gender)
try:
generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir),
r.get("composite_negative", ""),
size=size, prefix=pfx, gender=self._gender)
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)
if not paths:
return
+100
View File
@@ -11,6 +11,8 @@ import sqlite3
from pathlib import Path
from typing import Any, Dict, List, Optional
from PIL import Image
# 支持的图片格式:模特图/底图均按此识别(png/jpg 等常见格式全覆盖;AVIF/GIF/TIFF 亦支持)
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".avif", ".gif", ".tiff", ".tif"}
@@ -114,6 +116,104 @@ def find_first_model_folder(material_root, preferred: Optional[str] = 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 build_mark_model_map(db_path, material_root) -> Dict[str, str]:
"""启动任务前检测 spu.mark 字段,建立 {mark: 模特文件夹名} 字典。
规则:
- 读取 spu 表全部 mark 值(去重);
- 每个 mark 映射到 material_library/<mark> 目录(目录名与 mark 一致);
- 目录不存在时回退到 category 默认目录(T-shirt);
- 目前库中 mark=1 → 映射到 material_library/T-shirt。
"""
mark_map: Dict[str, str] = {}
try:
root = Path(material_root)
if not root.exists():
return mark_map
folders = [d.name for d in sorted(root.iterdir()) if d.is_dir()]
if not folders:
return mark_map
# 读 spu.mark 实际值(去重)
marks: List[str] = []
try:
conn = _connect(db_path)
rows = conn.execute("SELECT DISTINCT mark FROM SPU WHERE mark IS NOT NULL AND mark != ''").fetchall()
conn.close()
marks = [str(r["mark"]).strip() for r in rows if str(r["mark"]).strip()]
except Exception: # noqa: BLE001
marks = []
if not marks:
marks = ["1"] # 库无 mark 数据时按默认 1 处理
for m in marks:
if m in folders:
mark_map[m] = m
else:
# mark 无同名目录 → 回退默认 T-shirt(当前 mark=1 → T-shirt
mark_map[m] = "T-shirt" if "T-shirt" in folders else folders[0]
print(f"[product] mark→模特目录映射: {mark_map}")
except Exception as e: # noqa: BLE001
print(f"[product] mark→模特目录映射构建失败: {e}")
return mark_map
def find_model_images_for_mark(db_path, material_root, mark, category: str = "T-shirt",
ratio: float = 3 / 4, tolerance: float = 0.06) -> List[Path]:
"""按 spu.mark 定位模特目录,过滤非目标比例图片,返回合格图片列表。
- mark 有对应目录(material_library/<mark>)→ 用该目录;
- 否则回退 category(如 T-shirt);
- 过滤掉非 3:4 比例(默认容差 6%)的图片;
- 返回过滤后的图片列表(供调用方随机抽取)。
"""
root = Path(material_root)
if not root.exists():
return []
d = None
if mark is not None:
cand = root / str(mark)
if cand.is_dir():
d = cand
if d is None:
cand = root / category
if cand.is_dir():
d = cand
if d is None:
# 兜底:第一个有图的目录(跳过无图目录)
for sub in sorted(root.iterdir()):
if not sub.is_dir():
continue
if any(f.is_file() and f.suffix.lower() in IMG_EXTS for f in sub.iterdir()):
d = sub
break
if d is None:
return []
imgs = [f for f in sorted(d.iterdir()) if f.is_file() and f.suffix.lower() in IMG_EXTS]
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 first_available_sku(db_path, basemap_root, spu_code: str) -> Optional[str]:
"""返回该款号下第一个「本地有底图」的 SKU.code;无则 None。"""
for c in list_colors(db_path, spu_code):
+1
View File
@@ -33,6 +33,7 @@ class AgentState(TypedDict, total=False):
pinterest_search_terms: List[str] # pinterest_search 产出:LLM 生成的搜索词
pinterest_images: Dict[str, List[str]] # pinterest_scrape 产出:搜索词 → 爬取图片路径列表
pinterest_briefs: List[Dict[str, Any]] # pinterest_analyze 产出:LLM 分析图片的原始设计简报
pinterest_login: Dict[str, Any] # pinterest_scrape 产出:登录态检测结果 {status, detail, ...}
# —— Pinterest 按需搜索循环状态 ——
pinterest_target: int # 目标简报数(= spu_tasks 数量,每款一个设计)
+110 -9
View File
@@ -25,6 +25,34 @@ _COUNTRY_NAME_MAP = {
"沙特": "沙特阿拉伯",
}
# 成分值字典映射:db 成分值 → 女装模板下拉框选项(男装模板选项与 db 值一致,直接保留)。
# 女装模板(如 SatVoy 沙特)成分下拉框是「中文+英文」格式(棉Cotton),db 存中文(棉),需映射。
_COMPONENT_FEMALE_MAP = {
"": "棉Cotton",
"聚酯纤维": "聚酯纤维(涤纶)Polyester",
"氨纶": "氨纶Elastane",
"锦纶": "锦纶(尼龙)Polyamide",
"再生聚酯纤维": "聚酯纤维(涤纶)Polyester", # 女装无再生聚酯纤维,归入聚酯纤维
"棉Cotton": "棉Cotton", # 已是女装格式,保持
}
def _map_component(value: Any, gender: Optional[str]) -> Any:
"""成分值按性别映射:male/None 保留原值;female 查女装字典(找不到保留原值)。"""
if gender != "female" or value in (None, ""):
return value
return _COMPONENT_FEMALE_MAP.get(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:
"""把尺码字符串转成可排序 rank(从小到大)。
@@ -206,9 +234,11 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
color: Optional[str] = None,
fabric_headers: Optional[List[str]] = None) -> Dict[str, Any]:
fabric_headers: Optional[List[str]] = None,
gender: Optional[str] = None) -> Dict[str, Any]:
"""构造一行 SPU(固定字段:SKC货号=code、风格=休闲、商品产地=经营站点;多颜色时用色值列区分)。
fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。"""
fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。
component_1/2/3 按性别映射(gender=female 时查 _COMPONENT_FEMALE_MAP,男装/None 保留原值)。"""
row: Dict[str, Any] = {
"基础信息-商品层级": "spu",
"SKC货号": spu_code, # code 路由为 SKC货号(用户要求)
@@ -220,6 +250,8 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
row["色值(主规格)"] = color
for dbk, header in SPU_MAP.items():
v = spu.get(dbk)
if dbk in ("component_1", "component_2", "component_3"):
v = _map_component(v, gender)
if v not in (None, ""):
row[header] = v
fabric = spu.get("fabric")
@@ -251,15 +283,39 @@ def _find_sa_size_headers(router) -> List[str]:
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_suggested_required(router, col: int) -> bool:
"""读取「建议售价」列下方注意事项,判断是否必填。
结合「非必填/必填」判断(不能只看「必填」两字):含「非必填」→非必填;否则含「必填」→必填。"""
note = str(router.ws.cell(router.header_row + 1, col).value or "")
if "非必填" in note:
return False
return "必填" in note
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,
multi: bool = True, price_header: str = "申报价格-日本站",
bust_headers: Optional[List[str]] = None,
price_headers: Optional[List[str]] = None,
sa_size_headers: Optional[List[str]] = None) -> Dict[str, Any]:
sa_size_headers: Optional[List[str]] = None,
suggested_price_ratio: float = 0.0,
suggested_price_headers: Optional[List[str]] = None,
suggested_required: Optional[Dict[str, bool]] = None,
suggested_unit_headers: Optional[List[str]] = None) -> Dict[str, Any]:
"""构造一行 SKU(固定字段:SPU货号、SKC货号=sku.code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。
价格(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 参数值)。"""
row: Dict[str, Any] = {
@@ -295,6 +351,14 @@ 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+加价%)
for h in price_headers:
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:
if (suggested_required or {}).get(h, True):
row[h] = suggested
for uh in suggested_unit_headers:
row[uh] = "CNY"
continue
if dbk == "size":
if v in (None, ""):
@@ -445,6 +509,11 @@ def _insert_product_block(
bust_headers: Optional[List[str]] = None,
fabric_headers: Optional[List[str]] = None,
sa_size_headers: Optional[List[str]] = None,
gender: Optional[str] = None,
suggested_price_ratio: float = 0.0,
suggested_price_headers: Optional[List[str]] = None,
suggested_required: Optional[Dict[str, bool]] = None,
suggested_unit_headers: Optional[List[str]] = None,
) -> List[int]:
"""在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。
@@ -474,7 +543,8 @@ def _insert_product_block(
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
block_rows.append(router.insert(
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers),
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers,
gender=gender),
match="exact",
))
for ci, (sc, skus) in enumerate(skus_by_color):
@@ -488,7 +558,11 @@ def _insert_product_block(
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
markup_percent=markup_percent, multi=True,
bust_headers=bust_headers, price_headers=price_headers,
sa_size_headers=sa_size_headers),
sa_size_headers=sa_size_headers,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_required=suggested_required,
suggested_unit_headers=suggested_unit_headers),
spu_code=spu_code, match="exact",
))
@@ -504,7 +578,8 @@ def _insert_product_block(
else:
# 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
block_rows.append(router.insert(
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers), match="exact"))
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers,
gender=gender), match="exact"))
multi_variant = len(skus_by_color) > 1
for ci, (sc, skus) in enumerate(skus_by_color):
first = skus[0]
@@ -515,7 +590,11 @@ def _insert_product_block(
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
markup_percent=markup_percent, multi=multi_variant,
bust_headers=bust_headers, price_headers=price_headers,
sa_size_headers=sa_size_headers),
sa_size_headers=sa_size_headers,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_required=suggested_required,
suggested_unit_headers=suggested_unit_headers),
))
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)
@@ -554,6 +633,7 @@ def export_product(
seed_shot_urls: Optional[List[str]] = None,
append_to: str = "",
markup_percent: float = 0.0,
suggested_price_ratio: float = 0.0,
) -> Path:
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
@@ -584,6 +664,11 @@ def export_product(
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) # 建议售价单位列
suggested_required = {h: _read_suggested_required(router, router.column_map[h])
for h in suggested_price_headers} # 各建议售价列是否必填(结合非必填/必填判断)
_insert_product_block(router, db_path, spu_code, sku_code,
origin_province, warehouses, price_headers,
markup_percent=markup_percent, images=images,
@@ -594,7 +679,12 @@ def export_product(
seed_shot_urls=seed_shot_urls,
bust_headers=bust_headers,
fabric_headers=fabric_headers,
sa_size_headers=sa_size_headers)
sa_size_headers=sa_size_headers,
gender=gender,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_required=suggested_required,
suggested_unit_headers=suggested_unit_headers)
out = router.save(out_path)
return Path(out)
finally:
@@ -611,6 +701,7 @@ def export_products(
template_path: str,
out_path: str,
markup_percent: float = 0.0,
suggested_price_ratio: float = 0.0,
) -> Path:
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
@@ -628,6 +719,11 @@ def export_products(
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) # 建议售价单位列
suggested_required = {h: _read_suggested_required(router, router.column_map[h])
for h in suggested_price_headers} # 各建议售价列是否必填(结合非必填/必填判断)
for r in products:
try:
_insert_product_block(
@@ -647,6 +743,11 @@ def export_products(
bust_headers=bust_headers,
fabric_headers=fabric_headers,
sa_size_headers=sa_size_headers,
gender=gender,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_required=suggested_required,
suggested_unit_headers=suggested_unit_headers,
)
except Exception as e: # noqa: BLE001
print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}")
+24
View File
@@ -8,10 +8,34 @@
剔除非法记录并记录原因,保证下游拿到的数据"形状正确"
"""
import functools
import threading
import traceback
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 with_fallback(node_name: str):
"""装饰器:捕获节点异常,转为 state['errors'] 中的一条记录,返回空更新。
+90 -4
View File
@@ -5,7 +5,9 @@ import os
import random
import socket
import sys
import time
import urllib.request
from typing import Any, Dict
from urllib.parse import urlparse, quote
import aiohttp
@@ -218,6 +220,80 @@ def _session_user_data_dir() -> str:
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,
sem: asyncio.Semaphore, save_dir: str, proxy: str | None):
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)) # 滚轮事件间隔
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 已登录。
判定策略(避免误判):
@@ -292,7 +368,9 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
才认定未登录;否则默认已登录(不依赖可能不匹配的已登录选择器)。
- Pinterest 是 SPAgoto 后需等待渲染,否则瞬间误判未登录。
- 已登录 → 立即返回 True,走直路。
- 未登录(被弹回登录墙)→ 回退首页提示手动登录一次,登录成功后返回 True。
- 未登录
login_wait=True → 回退首页提示手动登录一次,登录成功后返回 True;
login_wait=False → 不阻塞,直接返回 False(由调用方跳过并告警)。
- 一直未登录(用户关窗口/放弃)→ 返回 False。
"""
async def _has_login_wall() -> bool:
@@ -327,6 +405,13 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
print("✅ 已检测到登录态,直接开始搜索")
return True
# 未登录:默认不阻塞(跳过并告警);login_wait=True 才回退首页等手动登录
if not login_wait:
print("⚠️ 未检测到 Pinterest 登录态(cookie 缺失或已过期)。"
"请先在 pinterest_scraper/.chrome_session 目录登录 Pinterest 一次,"
"或设置 pinterest.login_wait=true 让程序自动等待手动登录。")
return False
# 未登录(搜索页被弹回登录墙):回退首页,提示手动登录一次,成功后立即继续
print("⚠️ 当前未登录(搜索页被拦截)。请在弹出的浏览器窗口中手动登录,登录成功后将自动继续……")
try:
@@ -351,7 +436,8 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
async def scrape(keyword: str, count: int, headless: bool = False,
proxy: str | None = None,
cdp_url: str | None = None) -> set[str]:
cdp_url: str | None = None,
login_wait: bool = False) -> set[str]:
imgs_url: set[str] = set()
async with async_playwright() as p:
if cdp_url:
@@ -389,7 +475,7 @@ async def scrape(keyword: str, count: int, headless: bool = False,
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 cdp_url:
+5 -3
View File
@@ -19,11 +19,13 @@ from pinterest_scraper.pinterest_image_capture import (
def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = None,
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,返回下载成功的文件路径列表。
- proxy 为 None 时自动探测(环境变量/系统代理/本地常见端口);空字符串显式禁用。
- 首次运行会弹出本地 Chrome 窗口,需手动登录 Pinterest 一次,登录态缓存在
- login_wait=False(默认):运行时检测到未登录时不阻塞,直接返回空列表(由调用方跳过并告警);
login_wait=True:弹出 Chrome 窗口等待手动登录一次,登录态缓存在
pinterest_scraper/.chrome_session,之后自动复用。
"""
# 代理解析:显式传入 > 自动探测 > 直连
@@ -39,7 +41,7 @@ def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = No
os.makedirs(out, exist_ok=True)
print(f"[pinterest] 关键词: {keyword} | 数量: {count} | 保存目录: {out}")
imgs_url = asyncio.run(scrape(keyword, count, headless, proxy, cdp_url))
imgs_url = asyncio.run(scrape(keyword, count, headless, proxy, cdp_url, login_wait))
if not imgs_url:
print(f"[pinterest] 未收集到图片(可能未登录或搜索无结果): {keyword}")
return []
+12 -1
View File
@@ -406,7 +406,18 @@ class TemplateRouter:
# 2) 路由到目标行
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():
if value is None:
continue
-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 验证。