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:
@@ -21,6 +21,10 @@ logs/
|
|||||||
# Pinterest 登录态(首次手动登录后缓存,不入库)
|
# Pinterest 登录态(首次手动登录后缓存,不入库)
|
||||||
pinterest_scraper/.chrome_session/
|
pinterest_scraper/.chrome_session/
|
||||||
|
|
||||||
|
# 测试产物
|
||||||
|
_probe_out/
|
||||||
|
test_yunfei_concurrency.py
|
||||||
|
|
||||||
# 系统
|
# 系统
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ START → pinterest_init → pinterest_search → pinterest_scrape → pinterest
|
|||||||
**图池机制**:爬取的图片注册到 `image_pool.json`(路径+md5+搜索词);分析过的图片 md5 一律拉黑
|
**图池机制**:爬取的图片注册到 `image_pool.json`(路径+md5+搜索词);分析过的图片 md5 一律拉黑
|
||||||
(`used_images.json`,合适/不合适都拉黑);图池有未消费图片 → 直接分析(不搜索);不足 → 新一轮搜索。
|
(`used_images.json`,合适/不合适都拉黑);图池有未消费图片 → 直接分析(不搜索);不足 → 新一轮搜索。
|
||||||
每个简报带 `image_index`/`source_md5` 全局 id 校验,保证简报、设计稿、三合一套图对应同一张图。
|
每个简报带 `image_index`/`source_md5` 全局 id 校验,保证简报、设计稿、三合一套图对应同一张图。
|
||||||
|
分析前对图片做文件完整性校验(`PIL.verify()` 过滤损坏/截断图),避免把坏 base64 发给多模态网关
|
||||||
|
导致 400 拒收;分析失败(无有效图片/网关 400)时直接放弃该产品、跳过后续流程(**不做**纯文本降级、
|
||||||
|
**不做** mock 兜底),由路由自动取新图继续。
|
||||||
|
|
||||||
**并发控制**(`config.yaml` 的 `pinterest:`):`analyze_concurrency`(分析线程数)、`analyze_batch`
|
**并发控制**(`config.yaml` 的 `pinterest:`):`analyze_concurrency`(分析线程数)、`analyze_batch`
|
||||||
(每轮取图数,0=自动补齐让 pipeline 队列满并发)、`max_designs`(每搜索词简报上限)、
|
(每轮取图数,0=自动补齐让 pipeline 队列满并发)、`max_designs`(每搜索词简报上限)、
|
||||||
@@ -124,8 +127,26 @@ pod_trend_agent/
|
|||||||
设计稿生成后全局 MD5 去重(`.cache/global_design_md5.json`),跨国家生效。
|
设计稿生成后全局 MD5 去重(`.cache/global_design_md5.json`),跨国家生效。
|
||||||
5. **多模态分析**:大图先压缩(max 1024px / 1.5MB)再送 LLM,产出原创设计简报(motif/风格/配色/构图 +
|
5. **多模态分析**:大图先压缩(max 1024px / 1.5MB)再送 LLM,产出原创设计简报(motif/风格/配色/构图 +
|
||||||
risk_level:safe/review/blocked)。blocked 拦截、review 加原创化魔改引导、safe 直接生成。
|
risk_level:safe/review/blocked)。blocked 拦截、review 加原创化魔改引导、safe 直接生成。
|
||||||
|
每张图由多模态模型判定 `suitable_for_print`(是否适合做印花),False 的简报在节点层被过滤丢弃,
|
||||||
|
且该图 md5 立即拉黑,不进入生成流程。
|
||||||
6. **并发生成**:简报推入 `PinterestPipeline`,后台线程池(`product.concurrency`,默认 5)逐条
|
6. **并发生成**:简报推入 `PinterestPipeline`,后台线程池(`product.concurrency`,默认 5)逐条
|
||||||
生成 设计稿 → 三合一 → OSS 上传 → 种草图,边分析边生成,不等全部分析完。
|
生成 设计稿 → 三合一 → 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`
|
||||||
|
头,否则指数退避;5xx(500/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 信息(材质/成分/图案/领型/面料/克重/印花类型/尺码表/边长/包装重量…),
|
生成产品图后,从 db 读该 SPU/SKU 信息(材质/成分/图案/领型/面料/克重/印花类型/尺码表/边长/包装重量…),
|
||||||
经 `templates/template_router.py` 的路由函数填入商品上传模板,输出 `<SKU>_已填写.xlsx` 到
|
经 `templates/template_router.py` 的路由函数填入商品上传模板,输出 `{模板原文件名}_已填写.xlsx` 到
|
||||||
`output/<国家>/product/`。模板已自包含在项目 `templates/`(`config.product.template_path`,可改为自由上传)。
|
`output/<国家>/product/`。模板已自包含在项目 `templates/`(`config.product.template_path`,可改为自由上传)。
|
||||||
|
|
||||||
- 数据从模板第 6 行(数据区空行)开始填,SPU 与各尺码 SKU 紧邻;
|
- 数据从模板第 6 行(数据区空行)开始填,SPU 与各尺码 SKU 紧邻;
|
||||||
|
- **写入前按货号最后 3 位升序排序**:`template_export` 在批量填模板前取货号(`img_code`,前端
|
||||||
|
`code_prefix` 前缀命名的货号)末 3 位数字从小到大排序再顺序插入,保证 SPU/SKU 块按货号有序,
|
||||||
|
不受后台并发生成顺序影响;
|
||||||
|
- **不再产生空白 xlsx**:修复了旧版本探测模板可写性时误建空文件的 bug,product 目录只保留填好
|
||||||
|
的有效文件,文件名 = 模板原文件名 + `_已填写`(非固定「商品上传」);
|
||||||
- **列名模糊匹配**:模板列名按名称匹配(非固定行列),支持中/英/日变体;
|
- **列名模糊匹配**:模板列名按名称匹配(非固定行列),支持中/英/日变体;
|
||||||
- **胸围识别**:识别「基码表-胸围(cm)」「胸围全围(cm)」等含「胸围」的列,全部填 `sku.bust` 值;
|
- **胸围识别**:识别「基码表-胸围(cm)」「胸围全围(cm)」等含「胸围」的列,全部填 `sku.bust` 值;
|
||||||
- **申报价格**:模糊匹配所有含「申报价格」的列,统一按加价后价格填写(`price × (1+markup_percent%)`);
|
- **申报价格**:模糊匹配所有含「申报价格」的列,统一按加价后价格填写(`price × (1+markup_percent%)`);
|
||||||
|
|||||||
+9
-6
@@ -46,15 +46,18 @@ pinterest:
|
|||||||
seed_sample: 40 # 每次从国家种子池随机抽取多少个种子词给 LLM
|
seed_sample: 40 # 每次从国家种子池随机抽取多少个种子词给 LLM
|
||||||
max_used_terms_in_prompt: 100 # 已用搜索词最多注入 LLM 提示词的个数(防 token 超限)
|
max_used_terms_in_prompt: 100 # 已用搜索词最多注入 LLM 提示词的个数(防 token 超限)
|
||||||
images_per_term: 40 # 每个搜索词爬取图片数量
|
images_per_term: 40 # 每个搜索词爬取图片数量
|
||||||
analyze_per_term: 1 # 每次调用分析 1 张图(一张对应一个设计,一次 API 请求;简报带 image_index 全局 id 校验防错位)
|
analyze_per_term: 1 # 固定每次调用分析 1 张图(一次 API 请求一张,利于 LLM 注意力;简报带 brief_id uuid4 全局校验防错位)
|
||||||
analyze_concurrency: 6 # 并发分析线程数(多并发提取简报)
|
analyze_concurrency: 6 # 并发分析线程数(多并发提取简报)
|
||||||
analyze_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次补齐到目标所需/每词简报上限,让 pipeline 队列时刻满并发)
|
analyze_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次最多分析 max_designs 张;搜集完一批→生成一批简报,不够用了才主动采集)
|
||||||
max_designs: 10 # 每个搜索词最多生成多少个设计简报
|
max_designs: 10 # 每个搜索词最多生成多少个设计简报
|
||||||
ref_images_per_design: 1 # 生图时每个设计附带几张爬取图作为参考(发给生图模型)
|
ref_images_per_design: 1 # 生图时每个设计附带几张爬取图作为参考(发给生图模型)
|
||||||
supply_attempts: 3 # 设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数
|
supply_attempts: 3 # 设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数
|
||||||
err400_limit: 15 # 每个种子词累计 400(且错误含「内容/图片」)超限后放弃该种子词(多模态+生图合计)
|
err400_limit: 15 # 每个种子词累计 400(且错误含「内容/图片」)超限后放弃该种子词(多模态+生图合计)
|
||||||
scrape_concurrency: 1 # 同时爬取几个搜索词(共享 .chrome_session 登录态,Chrome 单例,必须=1)
|
scrape_concurrency: 1 # 同时爬取几个搜索词(共享 .chrome_session 登录态,Chrome 单例,必须=1)
|
||||||
headless: false # 爬取时是否无头(false=显示 Chrome 窗口,首次需手动登录)
|
headless: false # 爬取时是否无头(false=显示 Chrome 窗口,首次需手动登录)
|
||||||
|
proxy: "" # 图片下载代理(空=自动探测系统代理/VPN,默认走代理;配了如 http://127.0.0.1:7890 则用指定代理)
|
||||||
|
login_check: true # 爬取前静态检测 Pinterest 登录态(只读 .chrome_session cookies,不启动 Chrome;未登录则跳过本轮爬取并告警)
|
||||||
|
login_wait: false # 运行时检测到未登录时:false=跳过并告警(不阻塞);true=弹出 Chrome 等待手动登录
|
||||||
|
|
||||||
# 跨源融合权重(按 source 标签,无需和为 1)
|
# 跨源融合权重(按 source 标签,无需和为 1)
|
||||||
# 已下调 gt_trending(泛国家热点只作微弱信号),主力偏向 style+related(可印花型词)。
|
# 已下调 gt_trending(泛国家热点只作微弱信号),主力偏向 style+related(可印花型词)。
|
||||||
@@ -162,12 +165,12 @@ RULES:/n1.底色锁定:从图3提取衣服底色与面料,最终合成中必须1
|
|||||||
# backend 留空则不生成设计稿(product 节点会自行回退生成)。
|
# backend 留空则不生成设计稿(product 节点会自行回退生成)。
|
||||||
compose:
|
compose:
|
||||||
backend: "openai" # 生成设计稿的图像后端:openai(需 api_key)/ mock(占位);留空=不生成
|
backend: "openai" # 生成设计稿的图像后端:openai(需 api_key)/ mock(占位);留空=不生成
|
||||||
api_key: "sk-ef1f251f059f9c2f8cc397de4a84e96d65775e1f2d32bb13b69b23dafc24d111"
|
api_key: "sk-1CicJFACR4JCeLe6LTdsqSd0ffSrybMgmdfRiNfIlKok8MbS"
|
||||||
base_url: "https://api.tofastcode.xyz" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404
|
base_url: "https://img.yunfei.best/v1" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404
|
||||||
model: "gpt-image-2"
|
model: "gpt-image-2"
|
||||||
size: "1536x2048" # 合成图/种草图等非设计图的统一尺寸(3:4)
|
size: "1536x2048" # 合成图/种草图等非设计图的统一尺寸(3:4)
|
||||||
design_size: "1024x1024" # 纯印花设计稿尺寸(1:1)
|
design_size: "1024x1024" # 纯印花设计稿尺寸(1:1)
|
||||||
background: "transparent" # 生成透明背景 PNG(gpt-image 系列支持);留空=默认背景
|
execution_mode: "" # 网关异步/同步模式参数(仅 ai-media 等网关需要,如 "sync");yunfei 等标准 OpenAI 网关不认识该参数,留空=不传
|
||||||
seed: 0 # 随机种子:>0=固定(相同 seed 可复现,需网关支持);0/留空=每次随机
|
seed: 0 # 随机种子:>0=固定(相同 seed 可复现,需网关支持);0/留空=每次随机
|
||||||
design_count: 1 # 用前 N 个 safe 简报生成设计稿(product 取第一个复用)
|
design_count: 1 # 用前 N 个 safe 简报生成设计稿(product 取第一个复用)
|
||||||
design_workers: 5 # 设计稿并发生成线程数(仅热点采集模式 compose 节点用;Pinterest 模式走 pipeline 总并发 product.concurrency)
|
design_workers: 5 # 设计稿并发生成线程数(仅热点采集模式 compose 节点用;Pinterest 模式走 pipeline 总并发 product.concurrency)
|
||||||
@@ -208,9 +211,9 @@ product:
|
|||||||
sku_code: "" # 颜色编码(留空自动选该款第一个有底图的);多个颜色用逗号分隔(如 "DG015-VT01,DG015-DARK HEATHER")
|
sku_code: "" # 颜色编码(留空自动选该款第一个有底图的);多个颜色用逗号分隔(如 "DG015-VT01,DG015-DARK HEATHER")
|
||||||
spu_per_color: # 留空自动判定:单色=每色一SPU;多色=单SPU多色(UI 不选择模板模式);CLI --single-spu 可显式覆盖
|
spu_per_color: # 留空自动判定:单色=每色一SPU;多色=单SPU多色(UI 不选择模板模式);CLI --single-spu 可显式覆盖
|
||||||
markup_percent: 0 # 加价百分比(按 SKU 最低价,供后续定价使用)
|
markup_percent: 0 # 加价百分比(按 SKU 最低价,供后续定价使用)
|
||||||
|
suggested_price_ratio: 30 # 建议售价比例(%):建议售价 = 申报价格 × (1+此比例/100);模板「建议售价」必填时才填
|
||||||
code_prefix: "DG" # 货号前缀(OSS 上传 key:{国家}/{时间戳}/{前缀+3位计数}_{4位随机}.jpg,000 起最多 999)
|
code_prefix: "DG" # 货号前缀(OSS 上传 key:{国家}/{时间戳}/{前缀+3位计数}_{4位随机}.jpg,000 起最多 999)
|
||||||
backend: "mock" # openai / mock / 留空=跳过生成
|
backend: "mock" # openai / mock / 留空=跳过生成
|
||||||
background: "transparent" # 设计稿透明背景(product 后端生图时也传 background=transparent)
|
|
||||||
template_dir: "templates" # template_router.py 所在目录(项目内自包含)
|
template_dir: "templates" # template_router.py 所在目录(项目内自包含)
|
||||||
template_path: "templates/商品上传模版.xlsx" # 商品上传模板(可改为自由上传)
|
template_path: "templates/商品上传模版.xlsx" # 商品上传模板(可改为自由上传)
|
||||||
concurrency: 5 # Pinterest 模式 pipeline 总并发(设计生成→三合一→种草图→标题 同一管道串行,总并发=此值)
|
concurrency: 5 # Pinterest 模式 pipeline 总并发(设计生成→三合一→种草图→标题 同一管道串行,总并发=此值)
|
||||||
|
|||||||
+57
-44
@@ -1,75 +1,88 @@
|
|||||||
# 模特特征库 —— 跨境电商基础款服装展示专用(已剔除外部穿搭与夸张妆造干扰)
|
# 模特特征库 —— 跨境电商年轻款服装展示专用(全库年龄20-33岁)
|
||||||
# 占位符:[模特特征] 随机取用
|
# 占位符:[模特特征] 随机取用
|
||||||
# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装。
|
# 取用规则:
|
||||||
# 按性别分组:female=女模 / male=男模。
|
# 1. 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机。
|
||||||
# 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机。
|
# 2. 如需同一商品多图保持同一模特(一景多动作),首次随机取到后请固定复用,不要每张图重新随机。
|
||||||
|
# 3. 站点适配:北美/欧洲站优先「欧美系+多元族裔系」,东南亚站优先「东南亚裔」,日韩站优先「亚洲系」。
|
||||||
|
# 描述规范:每条 = 年龄 + 族裔/风格标签 + 发型发色 + 肤色肤质 + 气质表情(字段顺序统一,出图更稳定)。
|
||||||
|
# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装,不描述下半身。全库限定年轻人群。
|
||||||
|
|
||||||
model_features:
|
model_features:
|
||||||
female:
|
female:
|
||||||
# ========== 女模 - 亚洲/清新/通勤类(适合基础款、纯色T恤) ==========
|
# ========== 1. 亚洲系(适合基础款/日韩风/通勤) ==========
|
||||||
- "22岁亚洲清新女学生,通透伪素颜,黑直发,清瘦骨感"
|
- "22岁亚洲清新系女模,黑长直发,通透伪素颜,清瘦骨感,邻家亲和感"
|
||||||
- "25岁日韩系通勤女模,元气通透底妆,棕色微卷发,亲切感"
|
- "25岁日韩系通勤女模,棕色微卷发,元气通透底妆,亲切微笑感"
|
||||||
- "27岁亚洲极简风女模,冷白皮,无瑕光泽肌,利落黑色短发"
|
- "27岁亚洲极简风女模,利落黑色短发,冷白皮,无瑕光泽肌,知性气质"
|
||||||
- "29岁亚洲轻熟女模,骨相美,微雾感底妆,气质温婉"
|
- "29岁亚洲轻熟女模,微卷深棕发,微雾感底妆,骨相精致,温婉气质"
|
||||||
|
- "31岁亚洲职场御姐女模,中分黑长发,干净底妆,干练自信气场"
|
||||||
|
|
||||||
# ========== 女模 - 欧美/阳光/甜心类(适合印花T恤、度假风) ==========
|
# ========== 2. 欧美阳光系(适合印花T恤/度假风) ==========
|
||||||
- "20岁欧美甜心女模,金发碧眼,阳光加州感,健康小麦色肌肤"
|
- "20岁欧美甜心女模,金色长直发,碧眼,健康小麦色肌肤,阳光加州感笑容"
|
||||||
- "23岁法式慵懒女模,法式刘海,微卷棕发,通透红唇"
|
- "23岁法式慵懒女模,法式刘海微卷棕发,通透底妆,柔和红唇,松弛气质"
|
||||||
- "27岁欧美自然系女模,淡雀斑,大地色系淡妆,健康好气色" # 新增
|
- "27岁欧美自然系女模,浅棕长发,淡雀斑,大地色淡妆,健康好气色"
|
||||||
- "21岁迈阿密热辣女孩,金棕色大波浪,高光立体修容,健康古铜色肌肤"
|
- "21岁迈阿密热辣女孩,金棕色大波浪,健康古铜色肌肤,立体修容,明媚笑容"
|
||||||
- "24岁北美学院风女模,高马尾,通透元气妆,白皙肌肤"
|
- "24岁北美学院风女模,高马尾,白皙肌肤,通透元气妆,青春活力感"
|
||||||
- "34岁北美宝妈系女模,自然淡妆,栗色长发,亲切温暖的微笑" # 新增
|
- "26岁北美邻家系女模,栗色长发,自然淡妆,亲切温暖笑容"
|
||||||
|
|
||||||
# ========== 女模 - 欧美/街头/个性类(适合街头风、美式复古印花) ==========
|
# ========== 3. 欧美街头系(适合美式复古/街头印花) ==========
|
||||||
- "22岁纽约下城区酷女孩,挑染发色,清冷厌世感,苍白质肌肤"
|
- "22岁纽约下城区酷女孩,灰紫色挑染发,苍白冷感肌肤,酷感眼神"
|
||||||
- "28岁欧美高级中性模,凌乱短发,冷白皮,骨感清冷"
|
- "28岁欧美高级中性风女模,凌乱金色短发,冷白皮,骨感清冷高级脸"
|
||||||
- "26岁拉美混血风女模,深邃眼窝,野生挑眉,健康小麦色肌肤"
|
- "26岁拉美混血风女模,深棕长卷发,深邃眼窝,健康小麦色肌肤,野性眉形"
|
||||||
- "25岁Y2K千禧辣妹,银色挑染发,清透裸妆,微胖立体感"
|
- "25岁Y2K千禧辣妹,银色挑染长发,清透裸妆,微胖立体感,个性张扬"
|
||||||
|
|
||||||
# ========== 女模 - 极简/性冷淡/高街类(适合纯色基础款、卫衣) ==========
|
# ========== 4. 极简高街系(适合纯色基础款/卫衣) ==========
|
||||||
- "30岁北欧极简风女模,冷白皮,无瑕哑光肌,银色利落短发"
|
- "30岁北欧极简风女模,银色利落短发,冷白皮,无瑕哑光肌,疏离高级感"
|
||||||
- "29岁纽约高街潮流女模,冷感通透妆,深棕色直发,疏离眼神"
|
- "29岁纽约高街潮流女模,深棕色直发,冷感通透妆,清冷眼神"
|
||||||
- "31岁哥本哈根极简风设计师女模,利落黑色短发,黑白灰冷感气质"
|
- "31岁哥本哈根极简风设计师女模,利落黑色短发,黑白灰冷感气质"
|
||||||
- "28岁法式先锋艺术系女模,单边耳坠,冷感素颜,深金色发"
|
- "28岁法式先锋艺术系女模,深金色长发,冷感素颜,单边耳坠,艺术家气质"
|
||||||
|
|
||||||
# ========== 女模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备) ========== # 新增分组
|
# ========== 5. 多元族裔系(北美/欧洲站覆盖) ==========
|
||||||
- "26岁非裔自然卷女模,健康深色肌肤,光泽感底妆,明快笑容"
|
- "26岁非裔自然卷女模,健康深色肌肤,光泽感底妆,明快笑容"
|
||||||
- "29岁非裔都市丽人女模,利落编发,精致底妆,自信干练气场"
|
- "29岁非裔都市丽人女模,利落编发,精致底妆,自信干练气场"
|
||||||
- "24岁印度裔女模,浓密黑长发,深邃大眼,暖调蜜糖色肌肤"
|
- "24岁印度裔女模,浓密黑长发,深邃大眼,暖调蜜糖色肌肤"
|
||||||
- "27岁拉丁裔阳光女模,栗色卷发,蜜色肌肤,热情爽朗笑容"
|
- "27岁拉丁裔阳光女模,栗色卷发,蜜色肌肤,热情爽朗笑容"
|
||||||
- "32岁中东裔优雅女模,微卷黑发,轮廓深邃,端庄知性气质"
|
- "32岁中东裔优雅女模,微卷黑发,轮廓深邃,端庄知性气质"
|
||||||
- "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容" # 大码:覆盖plus size市场
|
- "25岁东南亚裔清甜女模,黑长直发,蜜糖色肌肤,元气亲和笑容"
|
||||||
|
|
||||||
|
# ========== 6. 大码系(Plus Size市场) ==========
|
||||||
|
- "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容"
|
||||||
|
- "30岁欧美大码甜心女模,金色微卷发,健康白皙肌肤,自信明媚笑容"
|
||||||
|
|
||||||
male:
|
male:
|
||||||
# ========== 男模 - 亚洲/盐系/清新类(适合基础款、日韩系) ==========
|
# ========== 1. 亚洲系(适合基础款/日韩风) ==========
|
||||||
- "23岁日系盐系男模,单眼皮,清瘦骨感,柔软黑发"
|
- "23岁日系盐系男模,柔软黑发,单眼皮,清瘦骨感,干净少年感"
|
||||||
- "25岁亚洲清冷系男模,冷白皮,碎发自热感,下颌线清晰"
|
- "25岁亚洲清冷系男模,碎发自然感,冷白皮,下颌线清晰,清冷气质"
|
||||||
- "28岁亚洲轻熟男模,干净利落的短发,健康肤色,稳重气质"
|
- "28岁亚洲轻熟男模,干净利落的短发,健康肤色,稳重气质"
|
||||||
- "21岁亚洲阳光男大,通透底妆,微卷黑发,清爽笑容"
|
- "21岁亚洲阳光系男模,微卷黑发,通透底妆,清爽笑容"
|
||||||
|
- "26岁亚洲运动系男模,利落黑色短发,健康小麦色肌肤,阳光笑容"
|
||||||
|
|
||||||
# ========== 男模 - 欧美/街头/运动类(适合美式复古、街头印花) ==========
|
# ========== 2. 欧美街头/运动系(适合美式复古/街头印花) ==========
|
||||||
- "20岁加州冲浪系男模,阳光金发,麦色健康肌,清爽笑容"
|
- "20岁加州冲浪系男模,阳光金发,麦色健康肌,清爽笑容"
|
||||||
- "24岁欧美健身教练型男,肌肉线条分明,小麦色肌肤,硬朗骨相"
|
- "24岁欧美健身教练型男,肌肉线条分明,小麦色肌肤,硬朗骨相"
|
||||||
- "22岁纽约街头滑板男模,脏辫,清冷厌世感,苍白肌肤"
|
- "22岁纽约街头滑板男模,深色脏辫,苍白肌肤,酷感眼神"
|
||||||
- "27岁美式机车硬汉男模,粗犷髂廓,络腮胡,硬挺气质"
|
- "27岁美式机车风男模,粗犷轮廓,短络腮胡,年轻硬朗气质"
|
||||||
- "35岁北美成熟奶爸系男模,浅胡茬,栗色短发,温暖亲切笑容" # 新增
|
- "26岁北美暖男系男模,浅胡茬,栗色短发,温暖亲切笑容"
|
||||||
|
|
||||||
# ========== 男模 - 欧美/商务/雅痞类(适合重磅纯色T恤、卫衣) ==========
|
# ========== 3. 商务/雅痞系(适合重磅纯色T恤/卫衣) ==========
|
||||||
- "25岁英伦雅痞型男,复古油头,短胡茬,熟男气质"
|
- "25岁英伦雅痞型男,复古油头,短胡茬,痞帅气质"
|
||||||
- "30岁华尔街商务精英男模,背头,沉稳内敛气质,干净皮肤"
|
- "30岁华尔街商务精英男模,背头,干净皮肤,沉稳内敛气质"
|
||||||
- "28岁意大利型男,深邃五官,意式凌乱卷发,健康肌肤"
|
- "28岁意大利型男,意式凌乱卷发,深邃五官,健康肌肤"
|
||||||
- "32岁法式儒雅大叔,微卷发,金丝眼镜,温润如玉气质"
|
- "26岁法式文艺男模,微卷棕发,金丝眼镜,温柔书卷气"
|
||||||
|
|
||||||
# ========== 男模 - 极简/暗黑/高街类(适合纯色基础款、暗黑风) ==========
|
# ========== 4. 极简高街系(适合纯色基础款/暗黑风) ==========
|
||||||
- "25岁欧美高冷男模,银白发色,苍白冷白皮,锋利眼神"
|
- "25岁欧美高冷男模,银白发色,苍白冷白皮,锋利眼神"
|
||||||
- "29岁柏林暗黑系青年,苍白皮肤,冷感眼神,黑色直发"
|
- "29岁柏林暗黑系青年,黑色直发,苍白皮肤,冷感眼神"
|
||||||
- "26岁东欧赛博风男模,高颧骨,清冷骨相,浅色唇"
|
- "26岁东欧赛博风男模,高颧骨,清冷骨相,浅色唇"
|
||||||
- "33岁北欧性冷淡风男艺术家,银色短发,疏离眼神,极简气质"
|
- "33岁北欧性冷淡风男艺术家,银色短发,疏离眼神,极简气质"
|
||||||
|
|
||||||
# ========== 男模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备) ========== # 新增分组
|
# ========== 5. 多元族裔系(北美/欧洲站覆盖) ==========
|
||||||
- "27岁非裔阳光型男模,健康深色肌肤,利落寸头,爽朗笑容"
|
- "27岁非裔阳光型男模,健康深色肌肤,利落寸头,爽朗笑容"
|
||||||
- "31岁非裔商务型男模,修剪整齐的短须,挺拔身姿,可靠稳重气场"
|
- "31岁非裔商务型男模,修剪整齐的短须,挺拔身姿,可靠稳重气场"
|
||||||
- "26岁拉丁裔热情男模,微卷黑发,蜜色肌肤,明亮笑容"
|
- "26岁拉丁裔热情男模,微卷黑发,蜜色肌肤,明亮笑容"
|
||||||
- "29岁印度裔斯文男模,黑短发,浓眉,温和儒雅气质"
|
- "29岁印度裔斯文男模,黑短发,浓眉,温和儒雅气质"
|
||||||
- "30岁中东裔深邃男模,黑色微卷发,轮廓立体,沉稳内敛"
|
- "30岁中东裔深邃男模,黑色微卷发,轮廓立体,沉稳内敛"
|
||||||
- "33岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容" # 大码:覆盖plus size市场
|
- "25岁东南亚裔阳光男模,黑色短发,蜜色肌肤,清爽亲和笑容"
|
||||||
|
|
||||||
|
# ========== 6. 大码系(Plus Size市场) ==========
|
||||||
|
- "29岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容"
|
||||||
|
- "27岁欧美大码阳光男模,浅络腮胡,魁梧体型,憨厚亲切笑容"
|
||||||
|
|||||||
@@ -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:
|
seed_shot_templates:
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 一、 高转化白底/灰底主图区(聚焦正面印花)
|
# 一、 高转化白底/灰底主图区(聚焦正面印花)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "纯白背景正面全展示"
|
- name: "纯白背景正面服装主体展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,身体正对镜头微侧15度,眼神直视镜头,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部位于画面最上沿且占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔自然,肩部放松下沉,双手自然垂放在身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白无缝背景纸前,身体正对镜头微侧15度,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "高级浅灰背景全展示"
|
- name: "高级浅灰背景服装主体展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔自然,肩部放松,双手自然垂放或单手插兜姿态收尾于身体两侧,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "纯白背景双手展示下摆"
|
- name: "纯白背景下摆与版型展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手背在身后或自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景半身构图,模特站在纯白背景纸前,眼神直视镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,重点同时覆盖胸前印花与下摆缝线,二者均须完整清晰;头部占比不超过画面高度的四分之一,五官弱化不做强调,禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,双手背于身后或自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,身体正对镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺及宽松版型。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "低饱和克莱因蓝背景棚拍"
|
- name: "低饱和克莱因蓝背景服装主体棚拍"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特在低饱和克莱因蓝的无缝背景纸前摆出具有张力的站姿,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔有张力,肩线平直,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在低饱和克莱因蓝的无缝背景纸前,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "纯白背景微动态走动展示"
|
- name: "纯白背景微转体动态展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,呈现正对镜头的微动态走动状态,整体呈现[服装风格]的调性,展示衣服正面印花在动态下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身向一侧微转约30度后回望镜头方向,肩部放松,双手自然垂放于身体两侧,转动幅度不得使印花侧转到画面边缘之外,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白无缝背景纸前,呈现轻微转体的微动态,展示衣服正面印花在转动下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 二、 面料与印花特写区(打消购买顾虑)
|
# 二、 面料与印花特写区(打消购买顾虑)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "纯白背景领口细节特写"
|
- name: "纯白背景领口细节特写"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示衣服的领口、袖口等工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装细节构图:镜头聚焦领口与上胸区域,取景范围为鼻底以下至腰部以上,画面主体是这件[商品名称]的领口罗纹、缝线工艺、肩部做工与上胸区域的印花上缘;模特面部仅保留唇部以下的局部作比例陪衬,禁止以五官为画面焦点,禁止把镜头对准脸部,禁止裁切所要展示的衣服细节部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特肩部放松下沉,双手不在画面内,绝对不可遮挡衣服任何部位。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,画面聚焦展示衣服的领口工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,85mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "明亮室内逆光面料透气质感展示"
|
- name: "明亮室内逆光面料透气质感展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,边缘形成强烈的轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是被逆光照透的面料与胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,在衣服边缘形成柔和轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "双手拉扯领口弹性展示"
|
- name: "双手拉扯领口弹性展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捏住领口两侧向外轻拉展示弹力,绝对不可将手放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装细节构图:镜头聚焦领口与上胸区域,取景范围为鼻底以下至腰部以上,画面主体是双手轻拉领口的拉伸状态、领口罗纹回弹纹理与上胸区域的印花上缘;模特面部仅保留唇部以下的局部作比例陪衬,禁止以五官为画面焦点,禁止把镜头对准脸部,禁止裁切所要展示的衣服细节部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸。【动作硬约束】模特双手捏住领口左右两侧边缘向外轻拉展示弹力,手部仅接触领口边缘线,绝对不可移向胸口中央遮挡印花。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,85mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "低头凝视正面印花特写"
|
- name: "低头凝视正面印花特写"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特站在纯白背景纸前,微微低头凝视衣服胸前的印花图案,整体呈现[服装风格]的调性,画面以斜俯视角度聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;模特微微低头使视线落在胸前印花上,头部随低头角度自然变小并略带透视缩短,成为氛围陪衬,画面的绝对视觉中心依然是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特微微低头注视胸前印花,头发不得垂落遮盖肩部与印花区域,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡衣服。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,85mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 三、 居家生活场景区(营造松弛感与舒适度)
|
# 三、 居家生活场景区(营造松弛感与舒适度)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "居家沙发坐姿场景"
|
- name: "居家沙发服装主体坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手自然放在腿上或沙发扶手上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特得体地坐在明亮现代极简风格的客厅布艺沙发上,身体正对镜头微侧,姿态松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的舒适度。室内采用柔和的高调漫反射自然光,背景为高度虚化的明亮居家环境。画面比例为3:4。柯达Portra 400胶片质感,色彩柔和明亮,35mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特端坐于沙发上,上半身自然挺直微侧,双手自然搭在大腿或沙发扶手上远离胸口,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特得体地坐在明亮现代极简风格的客厅布艺沙发上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的舒适度。室内采用柔和的高调漫反射自然光,背景为高度虚化的明亮居家环境。画面比例为3:4。柯达Portra 400胶片质感,色彩柔和明亮,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "居家落地窗自然光展示"
|
- name: "居家落地窗自然光服装主体展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在高层公寓明亮的落地窗前,身体正前方对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在高层公寓明亮的落地窗前,身体正面对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透,衣服受光充足色彩还原准确。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "周末清晨床头坐姿场景"
|
- name: "周末清晨床头服装主体坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手自然放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特得体地坐在床边,展现周末清晨的松弛感,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的亲肤无拘束感。室内采用清晨明亮的高调自然光,背景为高度虚化的卧室家具。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特端坐于床沿,上半身自然挺直,双手自然搭在大腿上远离胸口,被子或床品绝对不可堆叠遮挡衣服下摆。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特得体地坐在明亮的床边,展现周末清晨的松弛感,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的亲肤无拘束感。室内采用清晨明亮的高调自然光,背景为高度虚化的卧室环境。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "居家厨房场景"
|
- name: "居家厨房服装主体场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手操作厨具,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特站在现代开放式厨房的中岛台前,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止围裙等其他服饰遮挡衣服。【动作硬约束】模特站在中岛台侧面,上半身放松自然,双手轻搭台面边缘远离胸口,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮的现代开放式厨房中岛台旁,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "周末居家阅读场景"
|
- name: "周末居家阅读服装主体场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手必须捧书放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特盘腿坐在卧室的羊毛地毯上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料的柔软亲肤感。室内采用柔和的窗光侧逆光,背景为高度虚化的卧室床铺与暖色氛围灯。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止书本遮挡衣服。【动作硬约束】模特坐在明亮飘窗边,书本置于腰腹高度的画面下沿位置,上胸与胸前印花完全不被遮挡,另一只手自然垂放,绝对不可将书举至胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮的窗边阅读,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料的柔软亲肤感。室内采用柔和的窗光侧顺光保证衣服受光均匀,背景为高度虚化的温馨居家环境。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 四、 户外休闲/运动场景区(展现百搭与透气)
|
# 四、 户外休闲/运动场景区(展现百搭与透气)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "阳光草坪户外站姿"
|
- name: "阳光草坪服装主体站姿"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的城市林荫道上,身体正对镜头微侧,微微低头微笑,整体展现[服装风格]的氛围,凸显[材质]在自然行动中的透气与百搭。午后阳光透过树叶洒下明亮的光斑,背景为高度虚化的过往行人与都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,35mm镜头,f/1.7大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止强烈树影大面积压暗衣服。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光充足的城市林荫道上,身体正对镜头微侧,表情放松带浅笑,整体展现[服装风格]的氛围,凸显[材质]在自然光照下的透气与百搭。午后阳光透过树叶洒下明亮光斑,光斑只落在背景上不打在衣服表面,衣服受光均匀色彩准确。背景为高度虚化的都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,50mm镜头,f/1.7大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "都市街头OOTD站姿展示"
|
- name: "都市街头OOTD服装主体展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手拿咖啡杯放在腰侧,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的现代商业街区外墙前,身体正对镜头微靠在墙上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身微靠墙面,肩部放松,咖啡杯持有手收在腰侧画面最下沿,另一只手自然垂放,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光明媚的现代商业街区外墙前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,光线均匀打亮衣服正面,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "天台蓝天白云站姿场景"
|
- name: "天台蓝天白云服装主体场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光,无死角的展现服装颜色。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光均匀打在衣服上,无死角的还原服装真实颜色。画面比例为3:4。徕卡Q2摄影质感,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "自然公园站姿外景"
|
- name: "自然公园服装主体站姿外景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的公园草坪前,微风吹拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光,画面明亮通透,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止衣物被风吹乱遮盖印花。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光充足的公园绿植前,微风轻拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光板补光,衣服受光均匀色彩鲜亮,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。
|
||||||
|
|
||||||
- name: "街头滑板站姿场景"
|
- name: "滑板公园服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚踩在滑板上,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的滑板公园边缘,身体正对镜头,微风吹拂发丝,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮的阳光形成侧逆光,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/1.7光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光明媚的滑板公园旁,身体正对镜头,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮阳光形成轻微侧光并配合反光板给衣服暗部补光,保证印花区域无死黑,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,50mm镜头,f/1.7光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "林荫道骑行停靠场景"
|
- name: "林荫道骑行服装主体场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚点地双手握把,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特骑着一辆复古自行车停在两旁长满梧桐树的林荫小道上,转头看向镜头微笑,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性。明亮阳光透过树叶洒下光斑,背景为高度虚化的树干与明亮光斑。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特站在复古自行车旁,握车把的手位于画面最下沿腰腹高度,另一只手自然垂放,头微转向镜头方向,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特停在两旁长满梧桐树的林荫小道旁,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性。明亮阳光透过树叶洒下光斑,光斑只点缀背景避开衣服表面,衣服受光均匀,背景为高度虚化的树干与明亮光斑。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "海边沙滩站姿场景"
|
- name: "海边沙滩服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在细软的沙滩上,海风吹起衣摆,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,画面明亮高调,背景为高度虚化的蔚蓝海水、白沙滩与远处海平线。画面比例为3:4。索尼A7R4画质,35mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在细软的沙滩上,海风轻扬发丝,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,衣服过曝得到良好控制,色彩鲜明通透,背景为高度虚化的蔚蓝海水与远处海平线。画面比例为3:4。索尼A7R4画质,50mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。
|
||||||
|
|
||||||
- name: "露营帐篷坐姿场景"
|
- name: "露营帐篷服装主体坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手端着搪瓷杯放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的自然光,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,35mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特端坐于露营椅上,上半身自然挺直,搪瓷杯收在腰腹高度的画面下沿,另一只手自然垂放,绝对不可举杯遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的漫射自然光,衣服受光均匀色彩还原准确,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "浅水沙滩站姿场景"
|
- name: "海边浅滩服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在浅水沙滩边,海风吹起衣摆,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与飘逸感。光线为正午到下午的明亮阳光,海面反光形成自然补光,背景为高度虚化的蔚蓝大海、白沙滩与天空。画面比例为3:4。夏日清透色彩,高饱和度,35mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止海水打湿变色衣服。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在海边浅滩旁,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈感。光线为下午的明亮阳光,海面反光形成天然柔和补光打亮衣服正面,背景为高度虚化的蔚蓝大海与天空。画面比例为3:4。夏日清透色彩,高饱和度,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "热带绿植温室站姿场景"
|
- name: "热带绿植温室服装主体场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在长满大型龟背竹与天堂鸟的植物园温室中,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与透气感。光线为穿透玻璃顶的明亮自然光,形成明亮的光斑,背景为高度虚化的茂密绿植。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.4大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止植物枝叶伸入前景遮挡衣服。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,与身后绿植保持距离,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在长满大型龟背竹与天堂鸟的植物园温室中,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与透气感。光线为穿透玻璃顶的明亮均匀自然光,衣服表面光影干净无杂乱叶影,背景为高度虚化的茂密绿植。画面比例为3:4。富士Provia 400X色彩预设,50mm镜头,f/1.4大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "现代美术馆纯白站姿外景"
|
- name: "现代美术馆纯白服装主体外景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代美术馆纯白外墙前,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,55mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特背靠在明亮的现代美术馆纯白外墙前,气质清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,墙面反光为衣服提供均匀补光,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "山顶站姿场景"
|
- name: "山顶服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在山顶的岩石上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光,画面通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,35mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在山顶的岩石平台上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光并配合天空的自然柔光罩效果,衣服受光均匀色彩通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 五、 工作/通勤场景区(展现日常实用度)
|
# 五、 工作/通勤场景区(展现日常实用度)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "办公场景坐姿展示"
|
- name: "办公场景服装主体坐姿展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手放在桌上或键盘上不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮的极简办公桌前,身体正对镜头微侧,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,背景为高度虚化的电脑屏幕与办公桌绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止桌面物品升高遮挡衣服下摆。【动作硬约束】模特端坐于办公桌前,上半身自然挺直微侧,双手平放键盘或桌面上远离胸口,屏幕与桌面位于画面最下沿之外,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮的极简办公桌前,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,光线均匀铺满衣服正面,背景为高度虚化的办公环境与绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "咖啡馆坐姿场景"
|
- name: "咖啡馆服装主体坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,单手端着陶瓷咖啡杯放在嘴边不遮挡印花,另一只手自然放在桌上,绝对不可双手交叉抱胸。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮风格咖啡馆的窗边座位上,身体正对镜头微侧,姿态慵懒松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的柔软垂坠感。明亮的自然光从大窗户斜射进来,画面通透,背景为高度虚化的木质室内装潢、绿植与朦胧的咖啡蒸汽。画面比例为3:4。日系胶片质感,富士Provia 400X色彩预设,35mm镜头,f/1.8大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止咖啡杯与杯蒸汽遮挡衣服。【动作硬约束】模特端坐于窗边座位,上半身自然挺直微侧,咖啡杯托在手心置于腰腹高度的画面下沿,杯体远离胸口区域,另一只手自然放在桌面,绝对不可将杯子抬至胸前。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮风格咖啡馆的窗边座位上,姿态松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的柔软垂坠感。明亮自然光从窗户侧面射入并配合室内暖光补足衣服正面亮度,画面通透,背景为高度虚化的木质装潢与绿植。画面比例为3:4。日系胶片质感,富士Provia 400X色彩预设,50mm镜头,f/1.8大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "书店站姿翻阅场景"
|
- name: "书店服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捧书放在腰间不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在明亮的高大木质书架前翻阅一本精装书,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止书本遮挡衣服。【动作硬约束】模特上半身挺拔微侧,持书的手将书收在腰腹高度的画面下沿视线瞥向书页,另一只手自然垂放,绝对不可将书举起靠近胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮的高大木质书架前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,射灯角度经过调整避免在衣服上留下脸部阴影,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "复古家具店站姿场景"
|
- name: "复古家具店服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在摆满上世纪老物件的中古家具店内,眼神安静,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的质感。店内采用明亮的自然光漫反射,背景为高度虚化的胡桃木柜子与明亮光斑。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止家具道具伸入前景遮挡衣服。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,与周围家具保持安全距离,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在摆满上世纪老物件的中古家具店内,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的质感。店内采用明亮的自然光漫反射,环境家具均为背景虚化元素,不在衣服上投下明显阴影,背景为高度虚化的胡桃木柜与明亮光斑。画面比例为3:4。富士Provia 400X色彩预设,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "艺术画廊站姿场景"
|
- name: "艺术画廊服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在灯光柔和明亮的现代艺术画廊内,侧身凝视一幅巨大的抽象画后转过头来正对镜头,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业的顶光灯打出柔和的漫反射,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,35mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特由侧身观展状态转回正对镜头,上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在灯光柔和明亮的现代艺术画廊内,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业顶光灯打出均匀漫反射铺满衣服正面,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,50mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。
|
||||||
|
|
||||||
- name: "超市货架站姿场景"
|
- name: "超市货架服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手推车不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特推着购物车或站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,姿态俏皮个性,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源打亮环境,背景为高度虚化的彩色商品与灯轨。画面比例为3:4。赛博潮流摄影,35mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止彩色货架光线污染衣服色调。【动作硬约束】模特上半身挺拔,扶购物车的手位于画面最下沿,另一只手自然垂放,姿态俏皮个性,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源仅照亮背景,模特所在的前景由柔和补光灯独立打亮,确保衣服色彩不受环境色影响而偏色,背景为高度虚化的彩色商品。画面比例为3:4。赛博潮流摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 六、 情绪氛围与高亮特色场景区(提升品牌调性)
|
# 六、 情绪氛围与高亮特色场景区(提升品牌调性)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "明亮工业风咖啡馆站姿展示"
|
- name: "明亮工业风咖啡馆服装主体展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在明亮工业风咖啡馆的纯白水泥墙前,眼神清冷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,背景为高度虚化的明亮通道与绿植。画面比例为3:4。赛博潮流电商摄影,55mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮工业风咖啡馆的纯白水泥墙前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,衣服正面受光均匀立体,背景为高度虚化的明亮通道与绿植。画面比例为3:4。潮流电商摄影,85mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "录音棚站姿场景"
|
- name: "录音棚服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手调音不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特戴着监听耳机站在专业麦克风前调音,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用录音棚明亮的暖色聚光灯与整体白光对比,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止耳机线垂落遮挡印花。【动作硬约束】模特佩戴监听耳机站立于麦克风架旁,麦杆收至脸侧避开胸口区域,一只手轻轻扶住耳机耳罩位于画面侧缘,另一只手自然垂放,绝对不可让手部或设备遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特在明亮的录音棚内,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用明亮的暖色聚光灯配合正面柔光板补光,确保印花区域通透明亮,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "暖光清吧坐姿场景"
|
- name: "暖光清吧服装主体坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手把玩酒杯放在吧台上不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在氛围感清吧的吧台前,眼神微醺迷离,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄色点光源与背景微弱的白光对比,背景为高度虚化的酒瓶阵列与杯光交错。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止酒杯遮挡衣服。【动作硬约束】模特端坐于吧台前,上半身自然挺直微侧,酒杯托在手心置于吧台面的画面下沿高度,远离胸口,另一只手自然垂放,绝对不可举杯遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在氛围感清吧的吧台前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄主光源配合正面白色补光板,衣服正面曝光充足色彩不偏橙,背景为高度虚化的酒瓶阵列与明亮灯带。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "日落黄昏街头站姿场景"
|
- name: "日落黄昏街头服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手插兜不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特倚靠在城市明亮的天桥栏杆旁,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏下的视觉效果与[材质]的质感与垂感。采用日落前明亮的黄金时刻光线与边缘光,背景为高度虚化的明亮车流与城市剪影。画面比例为3:4。赛博朋克潮流摄影,35mm镜头,f/1.4大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止剪影化处理导致衣服发黑。【动作硬约束】模特上半身微倚天桥栏杆,肩部放松,双手自然垂放于身体两侧胯部附近,栏杆金属管避开衣服下摆高度,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特倚靠在城市明亮的天桥栏杆旁,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏暖光下的视觉效果。采用日落黄金时刻的明亮顺侧光,金色阳光直接铺满衣服正面而非逆光,色彩温暖且印花清晰锐利,背景为高度虚化的明亮车流与城市天际线。画面比例为3:4。潮流摄影,50mm镜头,f/1.4大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "旱冰场站姿场景"
|
- name: "旱冰场服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特穿着旱冰鞋站在复古旱冰场中央,姿态从容,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。明亮的紫粉色调与射灯打在模特身上,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,35mm镜头,f/1.4大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止场地彩光污染衣服原有色彩。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,姿态从容,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在复古旱冰场中央,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。场地采用明亮的主射灯独立照亮模特并配有正面柔光,衣服保持自身固有色不被环境粉紫光染色,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,50mm镜头,f/1.4大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "明亮的现代地下车库站姿场景"
|
- name: "明亮现代地下车库服装主体站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,眼神冷酷,身体转回正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯照明,画面清透无暗角,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。赛博朋克潮流摄影,55mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔背靠水泥柱,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯正面补光组合,画面清透无暗角,衣服受光均匀对比适中,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。潮流摄影,85mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|||||||
Binary file not shown.
+55
-14
@@ -26,6 +26,7 @@ from graph.nodes import (
|
|||||||
template_export_node,
|
template_export_node,
|
||||||
)
|
)
|
||||||
from graph.state import AgentState
|
from graph.state import AgentState
|
||||||
|
from graph.validate import with_fallback
|
||||||
|
|
||||||
|
|
||||||
def build_graph():
|
def build_graph():
|
||||||
@@ -59,8 +60,16 @@ def build_graph():
|
|||||||
|
|
||||||
|
|
||||||
def _pinterest_route(state: Dict[str, Any]) -> str:
|
def _pinterest_route(state: Dict[str, Any]) -> str:
|
||||||
"""图池路由:简报达标 → done;图池还有未消费图片 → analyze(继续分析,不搜索);
|
"""图池路由:按简报池实时需求补——简报池还有待处理/在途简报 → 不分析不采集;
|
||||||
图池不足 → search(新一轮搜索);轮次耗尽 → done。"""
|
简报池空闲 + 图池有未消费图片 → 补分析;简报池空闲 + 图池空 + 简报不足 → 搜索采集;
|
||||||
|
简报达标 → done。不按任务数量提前结束,先消耗图池存量,不够用了才主动采集。
|
||||||
|
致命 503(图像服务不可用)→ 立即终止,不再搜索/分析,直接收尾合成模板。"""
|
||||||
|
# 致命 503:图像服务不可用,重试无效 → 提前终止,未完成产品废弃,直接收尾
|
||||||
|
pipe = state.get("pinterest_pipeline")
|
||||||
|
if pipe is not None and hasattr(pipe, "is_fatal_503_aborted") and pipe.is_fatal_503_aborted():
|
||||||
|
print("[pinterest_route] ⛔ 图像服务 503 已终止任务,停止搜索/分析,直接收尾合成模板")
|
||||||
|
return "done"
|
||||||
|
|
||||||
target = int(state.get("pinterest_target") or 0)
|
target = int(state.get("pinterest_target") or 0)
|
||||||
if target <= 0:
|
if target <= 0:
|
||||||
target = 1
|
target = 1
|
||||||
@@ -72,11 +81,14 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
|
|||||||
if max_rounds <= 0:
|
if max_rounds <= 0:
|
||||||
max_rounds = max(target * 2, 5)
|
max_rounds = max(target * 2, 5)
|
||||||
|
|
||||||
if len(briefs) >= target:
|
# 简报池还有待处理/在途简报 → 先让后台消化,不分析新图也不采集
|
||||||
print(f"[pinterest_route] 简报已达目标 {len(briefs)}/{target},结束")
|
if pipe is not None and hasattr(pipe, "pending_count"):
|
||||||
return "done"
|
pending = pipe.pending_count()
|
||||||
|
if pending > 0:
|
||||||
|
# 日志由 pinterest_wait 节点进入时统一打印(阻塞等待消化,避免此处高频刷屏)
|
||||||
|
return "wait"
|
||||||
|
|
||||||
# 图池还有未消费图片 → 继续分析(不搜索)
|
# 简报池空闲 → 检查图池:还有未消费图片 → 补分析(不管简报离目标差多少,先把这批用完)
|
||||||
try:
|
try:
|
||||||
from graph.pinterest import load_image_pool, load_used_images, pool_unused_images
|
from graph.pinterest import load_image_pool, load_used_images, pool_unused_images
|
||||||
pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "")
|
pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "")
|
||||||
@@ -85,24 +97,31 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
|
|||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
unused = []
|
unused = []
|
||||||
if unused:
|
if unused:
|
||||||
print(f"[pinterest_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target})")
|
print(f"[pinterest_route] 简报池空闲,图池还有 {len(unused)} 张未消费图片,补分析"
|
||||||
|
f"(简报 {len(briefs)}/{target})")
|
||||||
return "analyze"
|
return "analyze"
|
||||||
|
|
||||||
# 图池不足 → 搜索
|
# 简报池空闲 + 图池空 → 检查简报是否已达标
|
||||||
|
if len(briefs) >= target:
|
||||||
|
print(f"[pinterest_route] 图池已空,简报已达目标 {len(briefs)}/{target},结束")
|
||||||
|
return "done"
|
||||||
|
|
||||||
|
# 简报池空闲 + 图池空 + 简报不达标 → 新一轮搜索采集
|
||||||
if rounds >= max_rounds:
|
if rounds >= max_rounds:
|
||||||
print(f"[pinterest_route] 已达最大轮次 {max_rounds},简报 {len(briefs)}/{target},按现有结果继续")
|
print(f"[pinterest_route] 已达最大轮次 {max_rounds},图池已空,简报 {len(briefs)}/{target},按现有结果继续")
|
||||||
return "done"
|
return "done"
|
||||||
if not terms and rounds > 0:
|
if not terms and rounds > 0:
|
||||||
print(f"[pinterest_route] 无可用搜索词,停止搜索(简报 {len(briefs)}/{target})")
|
print(f"[pinterest_route] 无可用搜索词,图池已空,停止搜索(简报 {len(briefs)}/{target})")
|
||||||
return "done"
|
return "done"
|
||||||
print(f"[pinterest_route] 图池不足,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target})")
|
print(f"[pinterest_route] 简报池空闲,图池不足,简报未达标,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target})")
|
||||||
return "search"
|
return "search"
|
||||||
|
|
||||||
|
|
||||||
def build_pinterest_graph():
|
def build_pinterest_graph():
|
||||||
"""Pinterest 参考模式图(按需搜索循环 + 简报池并发生成):
|
"""Pinterest 参考模式图(按需搜索循环 + 简报池并发生成):
|
||||||
pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze
|
pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze
|
||||||
→ [pinterest_route] 简报不足 → 回到 pinterest_search;达标 → pinterest_finalize
|
→ [pinterest_route] 简报池还有在途 → wait(等待后台消化)→ 回到路由;
|
||||||
|
简报池空闲 + 图池有图 → analyze;图池空 + 简报不足 → search;达标 → pinterest_finalize
|
||||||
(排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export
|
(排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export
|
||||||
"""
|
"""
|
||||||
from graph.nodes import (
|
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_finalize_node import pinterest_finalize_node
|
||||||
from graph.nodes.pinterest_init_node import pinterest_init_node
|
from graph.nodes.pinterest_init_node import pinterest_init_node
|
||||||
|
|
||||||
|
@with_fallback("pinterest_wait")
|
||||||
|
def _pinterest_wait(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""简报池还有在途简报时阻塞等待后台消化(最多 60s),避免轮询刷屏。
|
||||||
|
|
||||||
|
用 pipeline.wait_idle 的 Condition 阻塞等待(_process_one 完成即唤醒),
|
||||||
|
消化完才返回,路由重新判断;超时兜底返回,避免死等。
|
||||||
|
"""
|
||||||
|
pipe = state.get("pinterest_pipeline")
|
||||||
|
if pipe is not None and hasattr(pipe, "wait_idle"):
|
||||||
|
pipe.wait_idle(60)
|
||||||
|
else:
|
||||||
|
import time as _t
|
||||||
|
_t.sleep(5)
|
||||||
|
return {}
|
||||||
|
|
||||||
builder = StateGraph(AgentState)
|
builder = StateGraph(AgentState)
|
||||||
builder.add_node("pinterest_init", pinterest_init_node)
|
builder.add_node("pinterest_init", pinterest_init_node)
|
||||||
builder.add_node("pinterest_search", pinterest_search_node)
|
builder.add_node("pinterest_search", pinterest_search_node)
|
||||||
builder.add_node("pinterest_scrape", pinterest_scrape_node)
|
builder.add_node("pinterest_scrape", pinterest_scrape_node)
|
||||||
builder.add_node("pinterest_analyze", pinterest_analyze_node)
|
builder.add_node("pinterest_analyze", pinterest_analyze_node)
|
||||||
|
builder.add_node("pinterest_wait", _pinterest_wait)
|
||||||
builder.add_node("pinterest_finalize", pinterest_finalize_node)
|
builder.add_node("pinterest_finalize", pinterest_finalize_node)
|
||||||
builder.add_node("template_export", template_export_node)
|
builder.add_node("template_export", template_export_node)
|
||||||
|
|
||||||
@@ -126,10 +161,12 @@ def build_pinterest_graph():
|
|||||||
builder.add_edge("pinterest_search", "pinterest_scrape")
|
builder.add_edge("pinterest_search", "pinterest_scrape")
|
||||||
builder.add_edge("pinterest_scrape", "pinterest_analyze")
|
builder.add_edge("pinterest_scrape", "pinterest_analyze")
|
||||||
builder.add_conditional_edges("pinterest_analyze", _pinterest_route, {
|
builder.add_conditional_edges("pinterest_analyze", _pinterest_route, {
|
||||||
"analyze": "pinterest_analyze", # 图池还有未消费图片 → 继续分析(不搜索)
|
"analyze": "pinterest_analyze", # 简报池空闲 + 图池有未消费图片 → 补分析(不搜索)
|
||||||
"search": "pinterest_search", # 图池不足 → 新一轮搜索
|
"search": "pinterest_search", # 简报池空闲 + 图池空 + 简报不足 → 新一轮搜索
|
||||||
|
"wait": "pinterest_wait", # 简报池还有在途 → 等待后台消化
|
||||||
"done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池)
|
"done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池)
|
||||||
})
|
})
|
||||||
|
builder.add_edge("pinterest_wait", "pinterest_analyze")
|
||||||
builder.add_edge("pinterest_finalize", "template_export")
|
builder.add_edge("pinterest_finalize", "template_export")
|
||||||
builder.add_edge("template_export", END)
|
builder.add_edge("template_export", END)
|
||||||
return builder.compile()
|
return builder.compile()
|
||||||
@@ -232,5 +269,9 @@ def run_pinterest_ref(
|
|||||||
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1,
|
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1,
|
||||||
"pinterest_rounds": 0,
|
"pinterest_rounds": 0,
|
||||||
"pinterest_attempted": [],
|
"pinterest_attempted": [],
|
||||||
|
"pinterest_images": {},
|
||||||
|
"pinterest_briefs": [],
|
||||||
|
"pinterest_login": {},
|
||||||
|
"pinterest_pipeline": None,
|
||||||
}
|
}
|
||||||
return compiled.invoke(state)
|
return compiled.invoke(state)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
所有请求跳过环境代理(NO_PROXY),适配用户挂 VPN 时直连国内/自建网关。
|
所有请求跳过环境代理(NO_PROXY),适配用户挂 VPN 时直连国内/自建网关。
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -31,6 +32,17 @@ _FINAL_STATUS = ("succeeded", "completed", "done")
|
|||||||
_FAIL_STATUS = ("failed", "error")
|
_FAIL_STATUS = ("failed", "error")
|
||||||
|
|
||||||
|
|
||||||
|
def _retry_after(resp, attempt: int) -> int:
|
||||||
|
"""429 限流等待秒数:优先取网关 Retry-After 头,否则指数退避(3/6/9s)。"""
|
||||||
|
ra = resp.headers.get("Retry-After")
|
||||||
|
try:
|
||||||
|
if ra is not None and ra.isdigit():
|
||||||
|
return min(int(ra), 30)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
return 3 * (attempt + 1)
|
||||||
|
|
||||||
|
|
||||||
def _shrink_blob_to_2mb(img_path: str, blob: bytes, max_bytes: int = 2 * 1024 * 1024) -> bytes:
|
def _shrink_blob_to_2mb(img_path: str, blob: bytes, max_bytes: int = 2 * 1024 * 1024) -> bytes:
|
||||||
"""把大图压缩到 <max_bytes(默认 2MB)再上传:
|
"""把大图压缩到 <max_bytes(默认 2MB)再上传:
|
||||||
- 尺寸过大先缩放(合成输入 1600x2200 内足够);
|
- 尺寸过大先缩放(合成输入 1600x2200 内足够);
|
||||||
@@ -171,8 +183,12 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
"n": 1,
|
"n": 1,
|
||||||
"size": size or cfg.get("size", "1024x1024"),
|
"size": size or cfg.get("size", "1024x1024"),
|
||||||
"model": model,
|
"model": model,
|
||||||
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
|
|
||||||
}
|
}
|
||||||
|
# execution_mode 仅对明确支持的网关(如 ai-media)传;yunfei 等标准 OpenAI 网关
|
||||||
|
# 不认识该参数,硬传会导致空响应,故默认不传,仅 config 显式配置时才带上
|
||||||
|
em = str(cfg.get("execution_mode") or "").strip()
|
||||||
|
if em:
|
||||||
|
data["execution_mode"] = em
|
||||||
if seed is not None:
|
if seed is not None:
|
||||||
data["seed"] = seed
|
data["seed"] = seed
|
||||||
# 提交重试:异步路径不稳定 → 失败重试同步提交(最多 3 次);
|
# 提交重试:异步路径不稳定 → 失败重试同步提交(最多 3 次);
|
||||||
@@ -181,6 +197,20 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
resp = requests.post(f"{base_url}/images/edits", headers=headers, files=files,
|
resp = requests.post(f"{base_url}/images/edits", headers=headers, files=files,
|
||||||
data=data, timeout=300, proxies=NO_PROXY)
|
data=data, timeout=300, proxies=NO_PROXY)
|
||||||
|
if resp.status_code == 429:
|
||||||
|
# 限流:尊重网关负载退避等待再重试,避免硬撞雪崩
|
||||||
|
wait = _retry_after(resp, attempt)
|
||||||
|
print(f"[img] 图像 API 429 限流,等待 {wait}s 重试 {attempt + 1}/3")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
if resp.status_code >= 500:
|
||||||
|
# 5xx 服务端错误(500/502/503/504 超时等):网关临时故障/过载,退避后重试
|
||||||
|
body = resp.text or ""
|
||||||
|
last_err = f"图像 API {resp.status_code}: {body[:300]}"
|
||||||
|
wait = 3 * (attempt + 1)
|
||||||
|
print(f"[img] 图像 API {resp.status_code}(网关临时故障/超时),等待 {wait}s 重试 {attempt + 1}/3")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
body = resp.text or ""
|
body = resp.text or ""
|
||||||
if "content_policy" in body and attempt < 2:
|
if "content_policy" in body and attempt < 2:
|
||||||
@@ -193,18 +223,24 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
|
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
|
||||||
try:
|
try:
|
||||||
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path)
|
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
# 空/非 JSON 响应:多为网关过载返回空 body → 退避后重试,
|
||||||
|
# 避免即时重压触发网关 429(用户实测空响应风暴 → 429)
|
||||||
|
last_err = str(e)
|
||||||
|
wait = 3 * (attempt + 1)
|
||||||
|
print(f"[img] 网关空响应(JSON 解析失败),等待 {wait}s 重试提交 {attempt + 1}/3: {e}")
|
||||||
|
time.sleep(wait)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
last_err = str(e)
|
last_err = str(e)
|
||||||
print(f"[img] 第 {attempt + 1} 次提交异步失败,重试同步提交: {e}")
|
print(f"[img] 第 {attempt + 1} 次提交异步失败,等待后重试同步提交: {e}")
|
||||||
|
time.sleep(2 * (attempt + 1))
|
||||||
raise RuntimeError(f"图像合成多次提交均失败: {last_err}")
|
raise RuntimeError(f"图像合成多次提交均失败: {last_err}")
|
||||||
|
|
||||||
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "",
|
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "",
|
||||||
seed: Optional[int] = None) -> str:
|
seed: Optional[int] = None) -> str:
|
||||||
"""纯文生图:生成白底纯印花设计稿(standalone pure print design)。
|
"""纯文生图:生成白底纯印花设计稿(standalone pure print design)。
|
||||||
size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。
|
size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。
|
||||||
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
|
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。"""
|
||||||
background: 配置 compose.background="transparent" 时传 background 参数 → 透明背景 PNG
|
|
||||||
(gpt-image-1/2 等模型支持;网关不支持该参数时会被忽略或由网关兜底)。"""
|
|
||||||
cfg = self._cfg
|
cfg = self._cfg
|
||||||
api_key = cfg.get("api_key", "")
|
api_key = cfg.get("api_key", "")
|
||||||
model = cfg.get("model", "gpt-image-1")
|
model = cfg.get("model", "gpt-image-1")
|
||||||
@@ -220,17 +256,32 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
"size": size or cfg.get("size", "1024x1024"),
|
"size": size or cfg.get("size", "1024x1024"),
|
||||||
"model": model,
|
"model": model,
|
||||||
"response_format": "b64_json",
|
"response_format": "b64_json",
|
||||||
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
|
|
||||||
}
|
}
|
||||||
|
# execution_mode 仅对明确支持的网关(如 ai-media)传;yunfei 等标准 OpenAI 网关
|
||||||
|
# 不认识该参数,硬传会导致空响应,故默认不传,仅 config 显式配置时才带上
|
||||||
|
em = str(cfg.get("execution_mode") or "").strip()
|
||||||
|
if em:
|
||||||
|
data["execution_mode"] = em
|
||||||
if seed is not None:
|
if seed is not None:
|
||||||
data["seed"] = seed
|
data["seed"] = seed
|
||||||
bg = str(cfg.get("background") or "").strip()
|
|
||||||
if bg:
|
|
||||||
data["background"] = bg # 如 "transparent"(透明背景 PNG)
|
|
||||||
last_err: Optional[str] = None
|
last_err: Optional[str] = None
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
resp = requests.post(f"{base_url}/images/generations", headers=headers, json=data,
|
resp = requests.post(f"{base_url}/images/generations", headers=headers, json=data,
|
||||||
timeout=300, proxies=NO_PROXY)
|
timeout=300, proxies=NO_PROXY)
|
||||||
|
if resp.status_code == 429:
|
||||||
|
# 限流:尊重网关负载退避等待再重试,避免硬撞雪崩
|
||||||
|
wait = _retry_after(resp, attempt)
|
||||||
|
print(f"[img] 图像 API 429 限流,等待 {wait}s 重试 {attempt + 1}/3")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
if resp.status_code >= 500:
|
||||||
|
# 5xx 服务端错误(500/502/503/504 超时等):网关临时故障/过载,退避后重试
|
||||||
|
body = resp.text or ""
|
||||||
|
last_err = f"图像 API {resp.status_code}: {body[:300]}"
|
||||||
|
wait = 3 * (attempt + 1)
|
||||||
|
print(f"[img] 图像 API {resp.status_code}(网关临时故障/超时),等待 {wait}s 重试 {attempt + 1}/3")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
body = resp.text or ""
|
body = resp.text or ""
|
||||||
if "content_policy" in body and attempt < 2:
|
if "content_policy" in body and attempt < 2:
|
||||||
@@ -243,7 +294,15 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
|
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
|
||||||
try:
|
try:
|
||||||
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path)
|
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
# 空/非 JSON 响应:多为网关过载返回空 body → 退避后重试,
|
||||||
|
# 避免即时重压触发网关 429(用户实测空响应风暴 → 429)
|
||||||
|
last_err = str(e)
|
||||||
|
wait = 3 * (attempt + 1)
|
||||||
|
print(f"[img] 网关空响应(JSON 解析失败),等待 {wait}s 重试生成 {attempt + 1}/3: {e}")
|
||||||
|
time.sleep(wait)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
last_err = str(e)
|
last_err = str(e)
|
||||||
print(f"[img] 第 {attempt + 1} 次生成异步失败,重试同步提交: {e}")
|
print(f"[img] 第 {attempt + 1} 次生成异步失败,等待后重试同步提交: {e}")
|
||||||
|
time.sleep(2 * (attempt + 1))
|
||||||
raise RuntimeError(f"图像生成多次提交均失败: {last_err}")
|
raise RuntimeError(f"图像生成多次提交均失败: {last_err}")
|
||||||
|
|||||||
@@ -180,14 +180,11 @@ class MockBackend:
|
|||||||
paths = list(image_paths or [])
|
paths = list(image_paths or [])
|
||||||
return [{
|
return [{
|
||||||
"topic": term,
|
"topic": term,
|
||||||
"concept": f"(启发式兜底)围绕「{term}」做原创{art_style}风格印花",
|
"suitable_for_print": True,
|
||||||
"motif": motif,
|
|
||||||
"art_style": art_style,
|
|
||||||
"color_palette": palette,
|
|
||||||
"composition": composition,
|
|
||||||
"negative_prompt": negative,
|
"negative_prompt": negative,
|
||||||
"image_prompt": (f"{motif}, {art_style}, {palette}, {composition}, "
|
"image_prompt": (f"{motif}, {art_style}, {palette}, {composition}, "
|
||||||
f"original {art_style} t-shirt print design"),
|
f"original {art_style} t-shirt print design, "
|
||||||
|
f"no brand logo, no trademark, no character, no watermark"),
|
||||||
# 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应)
|
# 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应)
|
||||||
"ref_images": [str(paths[i])] if i < len(paths) else [],
|
"ref_images": [str(paths[i])] if i < len(paths) else [],
|
||||||
"source": "pinterest",
|
"source": "pinterest",
|
||||||
|
|||||||
@@ -318,31 +318,56 @@ def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
|
|||||||
# —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)——
|
# —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)——
|
||||||
# image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接),
|
# image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接),
|
||||||
# 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。
|
# 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。
|
||||||
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD (print-on-demand) T-shirt design analyst.
|
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD T-shirt design analyst. Given ONE Pinterest reference image,
|
||||||
You receive Pinterest reference images for one search term. For each image, extract the VISUAL CONCEPT
|
judge whether it can inspire a T-shirt print, then write an ORIGINAL design brief
|
||||||
(style, mood, motif, color palette, composition) that makes it appealing, then produce an ORIGINAL
|
capturing its vibe WITHOUT copying.
|
||||||
T-shirt print design brief that captures that VIBE WITHOUT copying the image.
|
Your image_prompt will be sent TOGETHER WITH this reference image to an image
|
||||||
|
generator, so it must actively override visual imitation.
|
||||||
|
|
||||||
RULES:
|
RULES:
|
||||||
- NEVER copy the image, never reproduce the exact artwork, characters, logos, or any text from it.
|
1. NO COPYING — never reproduce or closely imitate the reference's artwork,
|
||||||
- Extract only the abstract style/mood/motif concept as inspiration.
|
characters, layout or text. Deliberately change motif, arrangement and/or
|
||||||
- Produce an original, flat, print-ready design brief (no garment, no model, no background scene).
|
palette so the two read as clearly different works sharing only a general
|
||||||
- COPYRIGHT-SAFE: no brands, no logos, no characters, no celebrities, no real persons, no franchises.
|
style. Distill inspiration into generic style words (retro, y2k, minimal,
|
||||||
- AVOID: politics, religion, hate, violence, sexual content, alcohol, national flags.
|
grunge, boho, kawaii...); never imitate an identifiable artist/studio/IP
|
||||||
- motif: English, concrete central subject of the print (e.g. "a smiling cat with a fish", "geometric mountain layers").
|
style.
|
||||||
- art_style: English visual technique (e.g. "clean flat vector", "retro screen print").
|
2. FORBIDDEN — brand logos, trademarks, slogans, mascots, copyrighted
|
||||||
- color_palette: English colors (e.g. "sunset orange, cream, dusty blue").
|
characters, real people/celebrities, movie/game/anime/band IP, lyrics,
|
||||||
- composition: English layout (e.g. "centered emblem with balanced negative space").
|
even stylized or silhouette versions. Avoid politics, religion, violence,
|
||||||
- concept: Chinese, one sentence describing the design idea.
|
sexual content, alcohol, drugs, gambling, flags, death/occult themes.
|
||||||
- negative_prompt: what to avoid (real people, likeness, characters, logos, text).
|
3. FORM — ONE clear central subject with strong graphic composition;
|
||||||
- image_prompt: a COMPLETE, fluent English text-to-image prompt for generating the ORIGINAL flat print
|
print-ready standalone artwork. ANY colors are fine — rich palettes,
|
||||||
design artwork (the print itself, NOT a garment photo). Describe the motif, art style, colors, layout
|
gradients and detailed shading are all acceptable. Photographic references
|
||||||
and mood in natural English, as a standalone print. Do NOT include garment / shirt / model / mannequin /
|
may be rendered as detailed full-color illustrations, retro badges or
|
||||||
background-scene / watermark words. Do NOT mention any size or white-background suffix — a fixed
|
vintage stickers.
|
||||||
"small centered print on pure white" suffix will be appended automatically by the system.
|
|
||||||
|
|
||||||
Return JSON with the field "designs" (array of objects with keys:
|
TEXT: short ORIGINAL English wording (1-6 words) allowed; wrap exact words in
|
||||||
motif, art_style, color_palette, composition, concept, negative_prompt, image_prompt)."""
|
double quotes and demand exact spelling; integrate into composition. Never
|
||||||
|
reuse/translate reference text; no brand/band/movie names or famous slogans.
|
||||||
|
When unsure, omit.
|
||||||
|
|
||||||
|
suitable_for_print: DEFAULT TRUE for graphics, illustrations, badges, vector
|
||||||
|
art, typography posters, or prints on mockups (judge only the printed artwork).
|
||||||
|
FALSE only for: subjectless photo scenery, memes/screenshots/collages,
|
||||||
|
watermarked or very low-quality images, decor/food/candid photos with no
|
||||||
|
usable motif. Even when FALSE, still fill all fields so downstream never breaks.
|
||||||
|
|
||||||
|
image_prompt = two parts:
|
||||||
|
1) mandatory opener, e.g.: "Use the attached reference image only as loose
|
||||||
|
inspiration for overall mood, theme and era — do NOT reproduce, trace,
|
||||||
|
rearrange, recolor or closely imitate any element, character, layout or
|
||||||
|
text shown in it."
|
||||||
|
2) the new design: [central motif] + [style] + [color treatment] +
|
||||||
|
[composition] + [mood], plus quoted original text if used.
|
||||||
|
NEVER mention shirts, apparel, models, scenes, sizes, backgrounds or
|
||||||
|
watermarks — placement is handled externally.
|
||||||
|
|
||||||
|
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 = {
|
PINTEREST_ANALYZE_SCHEMA = {
|
||||||
"name": "pinterest_design_briefs",
|
"name": "pinterest_design_briefs",
|
||||||
@@ -354,17 +379,11 @@ PINTEREST_ANALYZE_SCHEMA = {
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"image_index": {"type": "integer"},
|
"suitable_for_print": {"type": "boolean"},
|
||||||
"motif": {"type": "string"},
|
|
||||||
"art_style": {"type": "string"},
|
|
||||||
"color_palette": {"type": "string"},
|
|
||||||
"composition": {"type": "string"},
|
|
||||||
"concept": {"type": "string"},
|
|
||||||
"negative_prompt": {"type": "string"},
|
"negative_prompt": {"type": "string"},
|
||||||
"image_prompt": {"type": "string"},
|
"image_prompt": {"type": "string"},
|
||||||
},
|
},
|
||||||
"required": ["image_index", "motif", "art_style", "color_palette",
|
"required": ["suitable_for_print", "negative_prompt", "image_prompt"],
|
||||||
"composition", "concept", "negative_prompt", "image_prompt"],
|
|
||||||
"additionalProperties": False,
|
"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 (
|
return (
|
||||||
f"Country: {country}\n"
|
"Analyze the attached image and produce one ORIGINAL T-shirt print design brief "
|
||||||
f"Pinterest search term: {term}\n"
|
"that captures its visual vibe without copying it."
|
||||||
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."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -587,8 +598,7 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
on_400=None) -> List[Dict[str, Any]]:
|
on_400=None) -> List[Dict[str, Any]]:
|
||||||
"""多模态分析 Pinterest 图片 → 原创设计简报列表。
|
"""多模态分析 Pinterest 图片 → 原创设计简报列表。
|
||||||
|
|
||||||
图片输入不被模型支持(纯文本模型 400)时自动降级为纯文本分析(仅用搜索词)。
|
图片输入失败/无有效图片时直接放弃(返回 [],不降级纯文本),由节点跳过该产品。
|
||||||
失败返回 [],由节点兜底(回退 mock 规则简报)。
|
|
||||||
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
|
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
|
||||||
"""
|
"""
|
||||||
cfg = self._cfg
|
cfg = self._cfg
|
||||||
@@ -605,11 +615,26 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
data_uris: List[str] = []
|
data_uris: List[str] = []
|
||||||
for p in image_paths:
|
for p in image_paths:
|
||||||
try:
|
try:
|
||||||
|
pk = Path(p)
|
||||||
|
raw = pk.read_bytes()
|
||||||
|
# 校验图片完整性:损坏/截断的图片会被火山方舟等多模态接口直接 400 拒绝,
|
||||||
|
# 必须滤掉后才能编码 base64(PIL 打开失败即视为损坏)。
|
||||||
|
if not raw or len(raw) < 100:
|
||||||
|
print(f"[pinterest_analyze] 图片文件过小/为空,跳过: {p} ({len(raw)}B)")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
_im = Image.open(pk)
|
||||||
|
_im.verify() # 校验文件头/结构,不完整则抛异常
|
||||||
|
_im.close()
|
||||||
|
except Exception as _ve: # noqa: BLE001
|
||||||
|
print(f"[pinterest_analyze] 图片损坏/不完整,跳过: {p} ({_ve})")
|
||||||
|
continue
|
||||||
import base64 as b64
|
import base64 as b64
|
||||||
mime = "image/png"
|
mime = "image/png"
|
||||||
if Path(p).suffix.lower() in (".jpg", ".jpeg"):
|
if pk.suffix.lower() in (".jpg", ".jpeg"):
|
||||||
mime = "image/jpeg"
|
mime = "image/jpeg"
|
||||||
data_uris.append(f"data:{mime};base64,{b64.b64encode(Path(p).read_bytes()).decode()}")
|
data_uris.append(f"data:{mime};base64,{b64.b64encode(raw).decode()}")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[pinterest_analyze] 图片读取失败 {p}: {e}")
|
print(f"[pinterest_analyze] 图片读取失败 {p}: {e}")
|
||||||
|
|
||||||
@@ -623,11 +648,10 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _call(use_images: bool) -> str:
|
def _call() -> str:
|
||||||
user_content: List[Any] = [
|
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 = {
|
payload = {
|
||||||
"model": model,
|
"model": model,
|
||||||
@@ -656,18 +680,14 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return str(resp.json()["choices"][0]["message"].get("content") or "")
|
return str(resp.json()["choices"][0]["message"].get("content") or "")
|
||||||
|
|
||||||
raw = ""
|
# 图片输入失败/无有效图片 → 直接放弃该产品(不降级纯文本),由节点跳过后续流程
|
||||||
if data_uris:
|
if not data_uris:
|
||||||
|
print("[pinterest_analyze] 无有效图片输入,放弃该产品(不降级纯文本)")
|
||||||
|
return []
|
||||||
try:
|
try:
|
||||||
raw = _call(use_images=True)
|
raw = _call()
|
||||||
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
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[pinterest_analyze] 分析失败: {e}")
|
print(f"[pinterest_analyze] 图片分析失败,放弃该产品(不降级纯文本): {e}")
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
parsed = _extract_json(raw)
|
parsed = _extract_json(raw)
|
||||||
@@ -687,11 +707,7 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
continue
|
continue
|
||||||
designs.append({
|
designs.append({
|
||||||
"topic": term,
|
"topic": term,
|
||||||
"concept": str(d.get("concept", "")).strip(),
|
"suitable_for_print": bool(d.get("suitable_for_print", True)),
|
||||||
"motif": str(d.get("motif", "")).strip(),
|
|
||||||
"art_style": str(d.get("art_style", "")).strip(),
|
|
||||||
"color_palette": str(d.get("color_palette", "")).strip(),
|
|
||||||
"composition": str(d.get("composition", "")).strip(),
|
|
||||||
"negative_prompt": str(d.get("negative_prompt", "")).strip(),
|
"negative_prompt": str(d.get("negative_prompt", "")).strip(),
|
||||||
"image_prompt": str(d.get("image_prompt", "")).strip(),
|
"image_prompt": str(d.get("image_prompt", "")).strip(),
|
||||||
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)
|
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from graph.validate import with_fallback
|
from graph.validate import ThreadSafeErrors, with_fallback
|
||||||
|
|
||||||
RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}
|
RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}
|
||||||
|
|
||||||
@@ -29,6 +29,18 @@ def _notify_400(on_400, exc) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_503(on_503, exc) -> None:
|
||||||
|
"""致命图像服务错误(503 / 账户不可用)时触发 on_503 回调(供调用方提前终止任务)。"""
|
||||||
|
if on_503 is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from graph.pinterest_pipeline import PinterestPipeline
|
||||||
|
if PinterestPipeline.is_fatal_503(exc):
|
||||||
|
on_503()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
|
def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
|
||||||
lines = [
|
lines = [
|
||||||
"# POD 印花设计简报(LLM 合规筛选 + 生图提示词)",
|
"# POD 印花设计简报(LLM 合规筛选 + 生图提示词)",
|
||||||
@@ -112,6 +124,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
|
|||||||
errors: List[Dict[str, Any]] = None,
|
errors: List[Dict[str, Any]] = None,
|
||||||
seed: Optional[int] = None,
|
seed: Optional[int] = None,
|
||||||
on_400=None,
|
on_400=None,
|
||||||
|
on_503=None,
|
||||||
size: str = "1024x1024") -> Optional[str]:
|
size: str = "1024x1024") -> Optional[str]:
|
||||||
"""生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。
|
"""生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。
|
||||||
|
|
||||||
@@ -123,6 +136,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
|
|||||||
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
|
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
|
||||||
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
|
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
|
||||||
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
|
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
|
||||||
|
on_503: 致命图像服务错误(503/账户不可用)时回调(供调用方提前终止任务)。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
|
from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
|
||||||
@@ -145,6 +159,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
|
|||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}")
|
print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}")
|
||||||
_notify_400(on_400, e)
|
_notify_400(on_400, e)
|
||||||
|
_notify_503(on_503, e)
|
||||||
out_path = ib.generate(
|
out_path = ib.generate(
|
||||||
img_prompt, str(design_dir / f"{out_stem}_design.png"),
|
img_prompt, str(design_dir / f"{out_stem}_design.png"),
|
||||||
brief.get("composite_negative", ""), size=size, seed=seed)
|
brief.get("composite_negative", ""), size=size, seed=seed)
|
||||||
@@ -161,6 +176,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
|
|||||||
return out_path
|
return out_path
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
_notify_400(on_400, e)
|
_notify_400(on_400, e)
|
||||||
|
_notify_503(on_503, e)
|
||||||
if errors is not None:
|
if errors is not None:
|
||||||
errors.append({"node": "compose", "type": type(e).__name__,
|
errors.append({"node": "compose", "type": type(e).__name__,
|
||||||
"message": f"设计稿生成失败 {brief.get('topic','')}: {e}", "trace": ""})
|
"message": f"设计稿生成失败 {brief.get('topic','')}: {e}", "trace": ""})
|
||||||
@@ -238,12 +254,13 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
def _gen_one(i: int, b: Dict[str, Any]):
|
def _gen_one(i: int, b: Dict[str, Any]):
|
||||||
"""单张设计稿生成(并发线程内调用,每设计一线程)。"""
|
"""单张设计稿生成(并发线程内调用,每设计一线程)。"""
|
||||||
out_path = generate_design(ib, b, design_dir, f"{country}_{i:02d}",
|
out_path = generate_design(ib, b, design_dir, f"{country}_{i:02d}",
|
||||||
state.get("errors"), seed=_seed,
|
_safe_errors, seed=_seed,
|
||||||
size=compose_cfg.get("design_size", "1024x1024"))
|
size=compose_cfg.get("design_size", "1024x1024"))
|
||||||
if out_path is None:
|
if out_path is None:
|
||||||
return i, b, None, None
|
return i, b, None, None
|
||||||
return i, b, out_path, None
|
return i, b, out_path, None
|
||||||
|
|
||||||
|
_safe_errors = ThreadSafeErrors()
|
||||||
targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
|
targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
|
||||||
# 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待
|
# 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待
|
||||||
workers = max(1, min(len(targets), int((config.get("compose") or {}).get("design_workers", 5))))
|
workers = max(1, min(len(targets), int((config.get("compose") or {}).get("design_workers", 5))))
|
||||||
@@ -263,6 +280,9 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
b["design_path"] = out_path
|
b["design_path"] = out_path
|
||||||
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})
|
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})
|
||||||
print(f"[compose] 印花设计稿已生成: {out_path}")
|
print(f"[compose] 印花设计稿已生成: {out_path}")
|
||||||
|
# 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加)
|
||||||
|
if len(_safe_errors):
|
||||||
|
state.setdefault("errors", []).extend(list(_safe_errors))
|
||||||
|
|
||||||
stats = dict(state.get("stats") or {})
|
stats = dict(state.get("stats") or {})
|
||||||
stats["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)}
|
stats["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
1. 先用种子词走数据源抓取(Google Trends 等),成功即用新数据;
|
1. 先用种子词走数据源抓取(Google Trends 等),成功即用新数据;
|
||||||
2. 抓取失败/无结果才回退 output/<国>/collected_keywords.json 旧缓存,保证流水线不中断。
|
2. 抓取失败/无结果才回退 output/<国>/collected_keywords.json 旧缓存,保证流水线不中断。
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from graph.sources import get_source
|
from graph.sources import get_source
|
||||||
@@ -44,11 +46,9 @@ def fetch_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
use_collected = (config.get("fetch") or {}).get("use_collected", True)
|
use_collected = (config.get("fetch") or {}).get("use_collected", True)
|
||||||
if use_collected:
|
if use_collected:
|
||||||
try:
|
try:
|
||||||
import json as _json
|
p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
|
||||||
from pathlib import Path as _Path
|
|
||||||
p = _Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
|
|
||||||
if p.exists():
|
if p.exists():
|
||||||
data = _json.loads(p.read_text(encoding="utf-8"))
|
data = json.loads(p.read_text(encoding="utf-8"))
|
||||||
cached_rows = data.get("keywords") or []
|
cached_rows = data.get("keywords") or []
|
||||||
if cached_rows:
|
if cached_rows:
|
||||||
rows = [dict(r) for r in cached_rows] # 已过滤去重的关键词
|
rows = [dict(r) for r in cached_rows] # 已过滤去重的关键词
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
2) 真实人物(名单 + Firstname Lastname 模式,仅对 gt_trending 源,避免误删风格词)
|
2) 真实人物(名单 + Firstname Lastname 模式,仅对 gt_trending 源,避免误删风格词)
|
||||||
3) 设计相关性(剔除泛新闻/科技/赛事词)
|
3) 设计相关性(剔除泛新闻/科技/赛事词)
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from graph.scoring import apply_blacklist, filter_design_relevance, filter_person_names, filter_query_noise
|
from graph.scoring import apply_blacklist, filter_design_relevance, filter_person_names, filter_query_noise
|
||||||
@@ -70,13 +73,11 @@ def filter_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
# 而不是只显示简报(design_briefs 仅含本次限量生成的热点)。
|
# 而不是只显示简报(design_briefs 仅含本次限量生成的热点)。
|
||||||
if kept:
|
if kept:
|
||||||
try:
|
try:
|
||||||
import json as _json
|
p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
|
||||||
from pathlib import Path as _Path
|
|
||||||
p = _Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json"
|
|
||||||
p.parent.mkdir(parents=True, exist_ok=True)
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
kws = [{"topic": r.get("topic", ""), "source": r.get("source", ""),
|
kws = [{"topic": r.get("topic", ""), "source": r.get("source", ""),
|
||||||
"kind": r.get("kind", ""), "raw_score": r.get("raw_score")} for r in kept]
|
"kind": r.get("kind", ""), "raw_score": r.get("raw_score")} for r in kept]
|
||||||
p.write_text(_json.dumps({"country": country,
|
p.write_text(json.dumps({"country": country,
|
||||||
"collected_at": _datetime_now(),
|
"collected_at": _datetime_now(),
|
||||||
"keywords": kws}, ensure_ascii=False, indent=2),
|
"keywords": kws}, ensure_ascii=False, indent=2),
|
||||||
encoding="utf-8")
|
encoding="utf-8")
|
||||||
@@ -88,5 +89,4 @@ def filter_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def _datetime_now() -> str:
|
def _datetime_now() -> str:
|
||||||
import time
|
|
||||||
return time.strftime("%Y-%m-%dT%H:%M:%S")
|
return time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
图池机制:
|
图池机制:
|
||||||
- 从持久化图池(image_pool.json)取「未消费」图片(md5 不在 used_images.json)。
|
- 从持久化图池(image_pool.json)取「未消费」图片(md5 不在 used_images.json)。
|
||||||
- 大图先压缩(内存占用过大 → 缩放/重编码)再送 LLM。
|
- 大图先压缩(内存占用过大 → 缩放/重编码)再送 LLM。
|
||||||
- 多并发分析(每批 analyze_per_term 张,并发 analyze_concurrency 线程)。
|
- 多并发分析(每批 1 张,并发 analyze_concurrency 线程;一次 API 请求一张图,利于 LLM 注意力)。
|
||||||
- 每张被分析的图片 md5 一律拉黑(used_images.json)——合适→产出简报→生成设计(设计 md5 全局拉黑见 compose);
|
- 每张被分析的图片 md5 一律拉黑(used_images.json)——合适→产出简报→生成设计(设计 md5 全局拉黑见 compose);
|
||||||
不合适→图片 md5 已拉黑→下一轮自动取下一张,不重复分析。
|
不合适→图片 md5 已拉黑→下一轮自动取下一张,不重复分析。
|
||||||
- 图池无未消费图片时返回空,由路由触发新一轮搜索。
|
- 图池无未消费图片时返回空,由路由触发新一轮搜索。
|
||||||
|
|
||||||
兜底链:LLM 多模态 → 纯文本降级(后端内部)→ mock 规则简报 → 空列表(下游跳过)。
|
兜底链:LLM 多模态分析 → 失败/无图直接放弃本轮(不降级纯文本、不做 mock 兜底)。
|
||||||
带 with_fallback:任何异常都不中断。
|
带 with_fallback:任何异常都不中断。
|
||||||
"""
|
"""
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import re
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
@@ -25,25 +25,14 @@ from graph.pinterest import (
|
|||||||
pool_unused_images,
|
pool_unused_images,
|
||||||
save_used_images,
|
save_used_images,
|
||||||
)
|
)
|
||||||
from graph.validate import with_fallback
|
from graph.validate import ThreadSafeErrors, with_fallback
|
||||||
|
|
||||||
# 明显不适合 T 恤印花的简报主体(启发式过滤;真实判定交给 LLM 搜索词引导)
|
|
||||||
_BRIEF_UNSUITABLE = re.compile(
|
|
||||||
r"\b(landscape|panorama|scenery|cityscape|street scene|interior|room decor|"
|
|
||||||
r"food photography|meal|dinner plate|recipe|makeup|nails|manicure|"
|
|
||||||
r"weather forecast|map|directions|photorealistic scene|realistic portrait)\b",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _brief_suitable(b: Dict[str, Any]) -> bool:
|
def _brief_suitable(b: Dict[str, Any]) -> bool:
|
||||||
"""简报是否适合做 T 恤印花:非侵权(blocked 拦截)+ 有主体 + 非明显非印花概念。"""
|
"""简报是否适合做 T 恤印花:非侵权(blocked 拦截)+ LLM 判定适合印花(suitable_for_print)。"""
|
||||||
if str(b.get("risk_level") or "").strip().lower() == "blocked":
|
if str(b.get("risk_level") or "").strip().lower() == "blocked":
|
||||||
return False
|
return False
|
||||||
motif = str(b.get("motif") or "").strip()
|
if not bool(b.get("suitable_for_print", True)):
|
||||||
if not motif:
|
|
||||||
return False
|
|
||||||
if _BRIEF_UNSUITABLE.search(motif):
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -75,25 +64,21 @@ def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str,
|
|||||||
n = seen_topics.get(base.lower(), 0)
|
n = seen_topics.get(base.lower(), 0)
|
||||||
seen_topics[base.lower()] = n + 1
|
seen_topics[base.lower()] = n + 1
|
||||||
topic = base if n == 0 else f"{base} #{n + 1}"
|
topic = base if n == 0 else f"{base} #{n + 1}"
|
||||||
motif = str(b.get("motif") or "").strip() or term
|
if not bool(b.get("suitable_for_print", True)):
|
||||||
if not motif:
|
continue # LLM 判定不适合做印花 → 丢弃(md5 已拉黑,下轮取新图)
|
||||||
continue
|
|
||||||
out.append({
|
out.append({
|
||||||
"country": country,
|
"country": country,
|
||||||
"topic": topic,
|
"topic": topic,
|
||||||
"risk_level": str(b.get("risk_level") or "safe").strip().lower() or "safe",
|
"risk_level": str(b.get("risk_level") or "safe").strip().lower() or "safe",
|
||||||
"safe_for_print": bool(b.get("safe_for_print", True)),
|
"safe_for_print": bool(b.get("safe_for_print", True)),
|
||||||
"suitable_for_print": bool(b.get("suitable_for_print", True)),
|
"suitable_for_print": True,
|
||||||
"design_category": classify(term),
|
"design_category": classify(term),
|
||||||
"concept": str(b.get("concept") or "").strip() or f"围绕「{term}」的原创印花设计",
|
"concept": f"围绕「{term}」的原创印花设计",
|
||||||
"motif": motif,
|
|
||||||
"art_style": str(b.get("art_style") or "").strip(),
|
|
||||||
"color_palette": str(b.get("color_palette") or "").strip(),
|
|
||||||
"composition": str(b.get("composition") or "").strip(),
|
|
||||||
"negative_prompt": str(b.get("negative_prompt") or "").strip(),
|
"negative_prompt": str(b.get("negative_prompt") or "").strip(),
|
||||||
"image_prompt": str(b.get("image_prompt") or "").strip(),
|
"image_prompt": str(b.get("image_prompt") or "").strip(),
|
||||||
"ref_images": [str(p) for p in (b.get("ref_images") or []) if str(p)],
|
"ref_images": [str(p) for p in (b.get("ref_images") or []) if str(p)],
|
||||||
"source_md5": str(b.get("source_md5") or "").strip().lower(),
|
"source_md5": str(b.get("source_md5") or "").strip().lower(),
|
||||||
|
"brief_id": str(b.get("brief_id") or "").strip(),
|
||||||
"slogan": "",
|
"slogan": "",
|
||||||
"score": 1.0,
|
"score": 1.0,
|
||||||
"confidence": 1.0,
|
"confidence": 1.0,
|
||||||
@@ -110,23 +95,25 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
errors = list(state.get("errors") or [])
|
errors = list(state.get("errors") or [])
|
||||||
|
|
||||||
pcfg = config.get("pinterest") or {}
|
pcfg = config.get("pinterest") or {}
|
||||||
analyze_per_term = int(pcfg.get("analyze_per_term", 1))
|
analyze_per_term = 1 # 固定一次 API 请求分析 1 张图(利于 LLM 注意力;不再用 analyze_per_term 配置)
|
||||||
concurrency = int(pcfg.get("analyze_concurrency", 3))
|
concurrency = int(pcfg.get("analyze_concurrency", 3))
|
||||||
max_designs = int(pcfg.get("max_designs", 10))
|
max_designs = int(pcfg.get("max_designs", 10))
|
||||||
n_ref = max(1, int(pcfg.get("ref_images_per_design", 1)))
|
n_ref = max(1, int(pcfg.get("ref_images_per_design", 1)))
|
||||||
provider = str(pcfg.get("provider") or "openai").strip().lower()
|
provider = str(pcfg.get("provider") or "openai").strip().lower()
|
||||||
|
|
||||||
# 按需分析:只取补齐到目标所需的图片数(batch_size 为上限,不超额分析),并按 md5 去重,
|
# 简报池还有待处理/在途简报 → 不分析新图(等后台消化完再由路由决定下一步)
|
||||||
# 保证同一图片内容(md5)不会同时被多条简报使用
|
pipe = state.get("pinterest_pipeline")
|
||||||
target = int(state.get("pinterest_target") or 0)
|
if pipe is not None and hasattr(pipe, "pending_count") and pipe.pending_count() > 0:
|
||||||
existing = state.get("briefs") or []
|
print(f"[pinterest_analyze] 简报池还有 {pipe.pending_count()} 条在途,本轮不分析")
|
||||||
remaining = max(0, target - len(existing))
|
return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
|
||||||
|
"errors": errors}
|
||||||
|
|
||||||
|
# 按需分析:一次把图池当前未消费图片全部分析成简报(搜集完一批→生成一批简报),
|
||||||
|
# 不再按目标任务数截断(不够用了才由路由触发新采集)。analyze_batch 可设上限保护配额。
|
||||||
batch_size = int(pcfg.get("analyze_batch", 0))
|
batch_size = int(pcfg.get("analyze_batch", 0))
|
||||||
if batch_size <= 0:
|
if batch_size <= 0:
|
||||||
# 自动:一次分析补齐到「目标所需」或「每词简报上限」的较小值(每张图→1条简报),
|
batch_size = max_designs # 自动:一次最多分析 max_designs 张(每张图→1条简报)
|
||||||
# 让 pipeline 队列一次有足够任务,时刻保持并发生成(避免每轮只推 6 条导致线程空转)
|
need = batch_size
|
||||||
batch_size = min(remaining, max_designs)
|
|
||||||
need = min(batch_size, remaining) if remaining > 0 else 0
|
|
||||||
|
|
||||||
# 1) 图池取未消费图片(md5 不在 used_images);无 → 返回空,路由触发搜索
|
# 1) 图池取未消费图片(md5 不在 used_images);无 → 返回空,路由触发搜索
|
||||||
pool = load_image_pool(output_dir, country)
|
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 [],
|
return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
|
||||||
"errors": errors}
|
"errors": errors}
|
||||||
|
|
||||||
if need <= 0:
|
|
||||||
print("[pinterest_analyze] 简报已达标,无需分析")
|
|
||||||
return {"pinterest_briefs": [], "briefs": state.get("briefs") or [],
|
|
||||||
"errors": errors}
|
|
||||||
seen_md5: set = set()
|
seen_md5: set = set()
|
||||||
batch: List[Dict[str, Any]] = []
|
batch: List[Dict[str, Any]] = []
|
||||||
for img in unused:
|
for img in unused:
|
||||||
@@ -148,34 +131,27 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if m and m in seen_md5:
|
if m and m in seen_md5:
|
||||||
continue # 同一图片内容(md5)不重复分析
|
continue # 同一图片内容(md5)不重复分析
|
||||||
seen_md5.add(m)
|
seen_md5.add(m)
|
||||||
|
img["brief_id"] = str(uuid.uuid4()) # md5 校验后分配全局唯一 id,用于图-简报对应校验
|
||||||
batch.append(img)
|
batch.append(img)
|
||||||
if len(batch) >= need:
|
if len(batch) >= need:
|
||||||
break
|
break
|
||||||
print(f"[pinterest_analyze] 图池取 {len(batch)} 张未消费图片分析(按需 {need},"
|
print(f"[pinterest_analyze] 图池取 {len(batch)} 张未消费图片分析(本批上限 {need},"
|
||||||
f"池剩余未消费 {len(unused) - len(batch)} 张,已消费 {len(used)} 张)")
|
f"池剩余未消费 {len(unused) - len(batch)} 张,已消费 {len(used)} 张)")
|
||||||
|
|
||||||
def _assign_refs(res: List[Dict[str, Any]], chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _assign_refs(res: List[Dict[str, Any]], chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
"""按简报的 image_index(LLM 返回)匹配它实际分析的图,写入 source_md5 + ref_images。
|
"""每批只分析 1 张图:简报按顺序对应 chunk 里唯一一张图,写入 source_md5 + ref_images + brief_id。
|
||||||
|
|
||||||
全局 id 校验:简报必须带 image_index(对应输入第几张图,0-based);
|
全局 id 校验:分析前已给每张图分配 uuid4(brief_id),简报回填同一 brief_id,
|
||||||
无 image_index(mock 兜底)→ 回退按顺序;无效/越界/重复 → 丢弃该简报(避免错位)。
|
保证图-简报严格对应(一次 API 请求一张图,无 image_index 错位问题)。
|
||||||
这样 analyze_per_term 可 >1 一次分析多张图提速,简报仍严格对应各自的图。
|
|
||||||
"""
|
"""
|
||||||
chunk_paths = [img["path"] for img in chunk]
|
chunk_paths = [img["path"] for img in chunk]
|
||||||
chunk_md5s = [str(img.get("md5") or "").strip().lower() for img in chunk]
|
chunk_md5s = [str(img.get("md5") or "").strip().lower() for img in chunk]
|
||||||
used_idx: set = set()
|
chunk_ids = [str(img.get("brief_id") or "") for img in chunk]
|
||||||
out: List[Dict[str, Any]] = []
|
out: List[Dict[str, Any]] = []
|
||||||
for i, b in enumerate(res):
|
for i, b in enumerate(res):
|
||||||
if not isinstance(b, dict):
|
if not isinstance(b, dict):
|
||||||
continue
|
continue
|
||||||
try:
|
idx = i if i < len(chunk_paths) else 0 # 一次一张:简报按顺序对应唯一图
|
||||||
idx = int(b.get("image_index"))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
idx = i # 无 image_index → 回退按顺序
|
|
||||||
if idx < 0 or idx >= len(chunk_paths) or idx in used_idx:
|
|
||||||
print(f"[pinterest_analyze] 简报 image_index={idx} 无效/重复,丢弃(避免图-简报错位)")
|
|
||||||
continue
|
|
||||||
used_idx.add(idx)
|
|
||||||
refs: List[str] = []
|
refs: List[str] = []
|
||||||
for k in range(n_ref):
|
for k in range(n_ref):
|
||||||
src = chunk_paths[(idx + k) % len(chunk_paths)]
|
src = chunk_paths[(idx + k) % len(chunk_paths)]
|
||||||
@@ -183,6 +159,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
refs.append(src)
|
refs.append(src)
|
||||||
b["ref_images"] = refs
|
b["ref_images"] = refs
|
||||||
b["source_md5"] = chunk_md5s[idx] if idx < len(chunk_md5s) else ""
|
b["source_md5"] = chunk_md5s[idx] if idx < len(chunk_md5s) else ""
|
||||||
|
b["brief_id"] = chunk_ids[idx] if idx < len(chunk_ids) else ""
|
||||||
out.append(b)
|
out.append(b)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -210,6 +187,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
|
|
||||||
# 4) 多并发分析(每线程分析一个 chunk;LLM 后端只读 self._cfg,线程安全)
|
# 4) 多并发分析(每线程分析一个 chunk;LLM 后端只读 self._cfg,线程安全)
|
||||||
raw_briefs: List[Dict[str, Any]] = []
|
raw_briefs: List[Dict[str, Any]] = []
|
||||||
|
_safe_errors = ThreadSafeErrors()
|
||||||
|
|
||||||
def _analyze_chunk(chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _analyze_chunk(chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
term = str(chunk[0].get("term") or "")
|
term = str(chunk[0].get("term") or "")
|
||||||
@@ -225,7 +203,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
# 把每条简报的来源图路径回填为原始图(压缩图仅用于分析,参考图用原图)
|
# 把每条简报的来源图路径回填为原始图(压缩图仅用于分析,参考图用原图)
|
||||||
return _assign_refs(res, chunk)
|
return _assign_refs(res, chunk)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
errors.append({"node": "pinterest_analyze", "type": type(e).__name__,
|
_safe_errors.append({"node": "pinterest_analyze", "type": type(e).__name__,
|
||||||
"message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""})
|
"message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""})
|
||||||
print(f"[pinterest_analyze] 「{term}」分析失败: {e}")
|
print(f"[pinterest_analyze] 「{term}」分析失败: {e}")
|
||||||
return []
|
return []
|
||||||
@@ -233,25 +211,19 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
|
|
||||||
workers = max(1, min(concurrency, len(chunks)))
|
workers = max(1, min(concurrency, len(chunks)))
|
||||||
if len(chunks) > 1:
|
if len(chunks) > 1:
|
||||||
print(f"[pinterest_analyze] 并发分析 {len(chunks)} 批({workers} 线程,每批 {analyze_per_term} 张)…")
|
print(f"[pinterest_analyze] 并发分析 {len(chunks)} 批({workers} 线程,每批 1 张)…")
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as _ex:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as _ex:
|
||||||
_futs = [_ex.submit(_analyze_chunk, c) for c in chunks]
|
_futs = [_ex.submit(_analyze_chunk, c) for c in chunks]
|
||||||
for _f in concurrent.futures.as_completed(_futs):
|
for _f in concurrent.futures.as_completed(_futs):
|
||||||
raw_briefs.extend(_f.result())
|
raw_briefs.extend(_f.result())
|
||||||
|
# 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加)
|
||||||
|
if len(_safe_errors):
|
||||||
|
errors.extend(list(_safe_errors))
|
||||||
|
|
||||||
# 5) 兜底:LLM 无结果 → mock 规则简报(零 API 成本,保证有设计可生成)
|
# 5) 真实 LLM 图片分析无结果(失败/无有效图片)→ 直接放弃本轮,跳过后续流程
|
||||||
|
# 不再 mock 兜底生成设计(用户要求:分析失败即放弃该产品)
|
||||||
if not raw_briefs:
|
if not raw_briefs:
|
||||||
try:
|
print("[pinterest_analyze] 图片分析无结果,放弃本轮简报(跳过后续流程)")
|
||||||
mock = get_backend("mock")
|
|
||||||
for chunk in chunks:
|
|
||||||
term = str(chunk[0].get("term") or "")
|
|
||||||
paths = [compressed_map.get(img["path"], img["path"]) for img in chunk]
|
|
||||||
res = mock.analyze_pinterest_images(paths, term, country) or []
|
|
||||||
_assign_refs(res, chunk)
|
|
||||||
raw_briefs.extend(res)
|
|
||||||
print(f"[pinterest_analyze] 兜底:mock 规则简报 {len(raw_briefs)} 条")
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
print(f"[pinterest_analyze] mock 兜底失败: {e}")
|
|
||||||
|
|
||||||
# 6) 本批所有图片 md5 一律拉黑(已消费,不再复用)——合适/不合适都拉黑
|
# 6) 本批所有图片 md5 一律拉黑(已消费,不再复用)——合适/不合适都拉黑
|
||||||
for img in batch:
|
for img in batch:
|
||||||
@@ -267,19 +239,20 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if len(kept) < len(raw_briefs):
|
if len(kept) < len(raw_briefs):
|
||||||
print(f"[pinterest_analyze] 简报过滤:{len(raw_briefs)} → {len(kept)} 条适合印花")
|
print(f"[pinterest_analyze] 简报过滤:{len(raw_briefs)} → {len(kept)} 条适合印花")
|
||||||
|
|
||||||
# 8) 上限 + 去重(同 motif+style 指纹只留一条)
|
# 8) 上限 + 去重(同 brief_id 只留一条;brief_id 为每张图 uuid4,天然唯一)
|
||||||
kept = kept[:max_designs]
|
kept = kept[:max_designs]
|
||||||
seen: set = set()
|
seen: set = set()
|
||||||
uniq: List[Dict[str, Any]] = []
|
uniq: List[Dict[str, Any]] = []
|
||||||
for b in kept:
|
for b in kept:
|
||||||
fp = f"{str(b.get('motif', '')).strip().lower()}|{str(b.get('art_style', '')).strip().lower()}"
|
fp = str(b.get("brief_id") or b.get("image_prompt") or "").strip().lower()
|
||||||
if fp in seen:
|
if not fp or fp in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(fp)
|
seen.add(fp)
|
||||||
uniq.append(b)
|
uniq.append(b)
|
||||||
kept = uniq
|
kept = uniq
|
||||||
|
|
||||||
# 9) 富化 → screened → prompt_node 装配提示词 → 标准 briefs(追加到累计,按需截断到目标数)
|
# 9) 富化 → screened → prompt_node 装配提示词 → 标准 briefs(追加到累计)
|
||||||
|
# 不再按目标任务数截断:搜集完一批→生成一批简报,不够用了才由路由触发新采集
|
||||||
existing_topics = [str(b.get("topic", "")).strip() for b in (state.get("briefs") or [])]
|
existing_topics = [str(b.get("topic", "")).strip() for b in (state.get("briefs") or [])]
|
||||||
screened = _enrich_briefs(kept, country, existing_topics)
|
screened = _enrich_briefs(kept, country, existing_topics)
|
||||||
new_briefs: List[Dict[str, Any]] = []
|
new_briefs: List[Dict[str, Any]] = []
|
||||||
@@ -290,10 +263,6 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
old_count = len(state.get("briefs") or [])
|
old_count = len(state.get("briefs") or [])
|
||||||
accumulated = list(state.get("briefs") or [])
|
accumulated = list(state.get("briefs") or [])
|
||||||
accumulated.extend(new_briefs)
|
accumulated.extend(new_briefs)
|
||||||
target = int(state.get("pinterest_target") or 0)
|
|
||||||
if target > 0 and len(accumulated) > target:
|
|
||||||
accumulated = accumulated[:target]
|
|
||||||
print(f"[pinterest_analyze] 简报已达目标 {target} 条,截断多余部分")
|
|
||||||
|
|
||||||
# 推送实际新增且保留的简报到并发生成流水线(简报池):边分析边生成设计/三合一/种草图
|
# 推送实际新增且保留的简报到并发生成流水线(简报池):边分析边生成设计/三合一/种草图
|
||||||
pushed = accumulated[old_count:]
|
pushed = accumulated[old_count:]
|
||||||
|
|||||||
@@ -55,6 +55,31 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
headless = bool(pcfg.get("headless", False))
|
headless = bool(pcfg.get("headless", False))
|
||||||
proxy = pcfg.get("proxy") or None
|
proxy = pcfg.get("proxy") or None
|
||||||
search_mode = str(pcfg.get("search_mode") or "direct").strip().lower()
|
search_mode = str(pcfg.get("search_mode") or "direct").strip().lower()
|
||||||
|
login_check = bool(pcfg.get("login_check", True))
|
||||||
|
login_wait = bool(pcfg.get("login_wait", False))
|
||||||
|
|
||||||
|
# 爬取前静态检测 Pinterest 登录态(不启动 Chrome,只读 .chrome_session cookies):
|
||||||
|
# 未登录/无会话 → 跳过本轮爬取并告警,避免每个搜索词都启动 Chrome 后才发现未登录。
|
||||||
|
login_state: Dict[str, Any] = {"status": "unknown"}
|
||||||
|
if login_check:
|
||||||
|
try:
|
||||||
|
from pinterest_scraper.pinterest_image_capture import check_login_state
|
||||||
|
login_state = check_login_state()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
login_state = {"status": "unknown", "detail": f"登录态检测失败: {e}"}
|
||||||
|
status = login_state.get("status")
|
||||||
|
if status in ("logged_out", "no_session"):
|
||||||
|
print(f"[pinterest_scrape] ⚠️ 未检测到 Pinterest 登录态({login_state.get('detail')})。"
|
||||||
|
f"跳过本轮 {len(terms)} 个搜索词爬取。")
|
||||||
|
stats = dict(state.get("stats") or {})
|
||||||
|
stats["pinterest_scrape"] = {
|
||||||
|
"terms": len(terms), "scraped": 0, "skipped": 0, "failed": 0,
|
||||||
|
"images": 0, "pool": len((load_image_pool(output_dir, country).get("images")) or []),
|
||||||
|
"login_status": status,
|
||||||
|
}
|
||||||
|
return {"pinterest_images": {}, "pinterest_attempted": state.get("pinterest_attempted") or [],
|
||||||
|
"pinterest_login": login_state, "stats": stats, "errors": errors}
|
||||||
|
print(f"[pinterest_scrape] 登录态检测:{status}({login_state.get('detail')})")
|
||||||
|
|
||||||
# 所有搜索词共享同一个 .chrome_session 登录态目录,Chrome 对同一 user-data-dir 是单例,
|
# 所有搜索词共享同一个 .chrome_session 登录态目录,Chrome 对同一 user-data-dir 是单例,
|
||||||
# 并发启动会互相抢占导致 "browser has been closed",必须串行爬取。
|
# 并发启动会互相抢占导致 "browser has been closed",必须串行爬取。
|
||||||
@@ -62,7 +87,8 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
print(f"[pinterest_scrape] 共享登录态目录不支持并发,scrape_concurrency 强制为 1(原 {concurrency})")
|
print(f"[pinterest_scrape] 共享登录态目录不支持并发,scrape_concurrency 强制为 1(原 {concurrency})")
|
||||||
concurrency = 1
|
concurrency = 1
|
||||||
|
|
||||||
# 一次性探测并校验代理(Pinterest 需代理才能访问;代理失效时给出明确警告,避免逐词静默失败)
|
# 默认走代理:config.pinterest.proxy 未配置时自动探测(环境变量/系统代理/本地常见端口),
|
||||||
|
# 本地 VPN 已开启时探测到的代理即可访问 Pinterest;代理失效时给出明确警告,避免逐词静默失败。
|
||||||
if proxy is None:
|
if proxy is None:
|
||||||
try:
|
try:
|
||||||
from pinterest_scraper.pinterest_image_capture import detect_proxy, get_system_proxy, _validate_proxy
|
from pinterest_scraper.pinterest_image_capture import detect_proxy, get_system_proxy, _validate_proxy
|
||||||
@@ -91,7 +117,8 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
try:
|
try:
|
||||||
from pinterest_scraper.scraper import scrape_pinterest
|
from pinterest_scraper.scraper import scrape_pinterest
|
||||||
files = scrape_pinterest(term, count=images_per_term,
|
files = scrape_pinterest(term, count=images_per_term,
|
||||||
save_dir=str(term_dir), proxy=proxy, headless=headless)
|
save_dir=str(term_dir), proxy=proxy, headless=headless,
|
||||||
|
login_wait=login_wait)
|
||||||
results[term] = files
|
results[term] = files
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
failed.append(term)
|
failed.append(term)
|
||||||
@@ -150,8 +177,9 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
stats["pinterest_scrape"] = {
|
stats["pinterest_scrape"] = {
|
||||||
"terms": len(terms), "scraped": len(results), "skipped": len(skipped),
|
"terms": len(terms), "scraped": len(results), "skipped": len(skipped),
|
||||||
"failed": len(failed), "images": total, "pool": len(pool.get("images") or []),
|
"failed": len(failed), "images": total, "pool": len(pool.get("images") or []),
|
||||||
|
"login_status": login_state.get("status", "unknown"),
|
||||||
}
|
}
|
||||||
print(f"[pinterest_scrape] 完成:{len(results)} 个搜索词,共 {total} 张图(跳过 {len(skipped)},失败 {len(failed)})")
|
print(f"[pinterest_scrape] 完成:{len(results)} 个搜索词,共 {total} 张图(跳过 {len(skipped)},失败 {len(failed)})")
|
||||||
|
|
||||||
return {"pinterest_images": results, "pinterest_attempted": attempted,
|
return {"pinterest_images": results, "pinterest_attempted": attempted,
|
||||||
"stats": stats, "errors": errors}
|
"pinterest_login": login_state, "stats": stats, "errors": errors}
|
||||||
|
|||||||
+82
-26
@@ -34,22 +34,18 @@ from typing import Any, Dict, List, Optional
|
|||||||
from graph.paths import project_root, runtime_root
|
from graph.paths import project_root, runtime_root
|
||||||
from graph.product import (
|
from graph.product import (
|
||||||
find_basemap,
|
find_basemap,
|
||||||
find_first_model_folder,
|
|
||||||
first_available_sku,
|
first_available_sku,
|
||||||
list_colors,
|
list_colors,
|
||||||
list_spus,
|
list_spus,
|
||||||
)
|
)
|
||||||
from graph.validate import with_fallback
|
from graph.validate import ThreadSafeErrors, with_fallback
|
||||||
|
|
||||||
|
|
||||||
_USED_LOCK = threading.Lock() # used_designs.json 并发写锁
|
_USED_LOCK = threading.Lock() # used_designs.json 并发写锁
|
||||||
_MODEL_LOCK = threading.Lock() # 同款共用模特缓存并发锁
|
|
||||||
_MODEL_CACHE: Dict[str, Any] = {} # 同款共用模特:spu_code → model 路径
|
|
||||||
|
|
||||||
|
|
||||||
def _next_img_idx(prod_dir: Path, prefix: str) -> int:
|
def _next_img_idx(prod_dir: Path, prefix: str) -> int:
|
||||||
"""货号续号:扫 prod_dir 已有 {prefix}{数字}* 文件,返回下一个起始序号(不覆盖旧产物)。"""
|
"""货号续号:扫 prod_dir 已有 {prefix}{数字}* 文件,返回下一个起始序号(不覆盖旧产物)。"""
|
||||||
import re
|
|
||||||
max_n = -1
|
max_n = -1
|
||||||
try:
|
try:
|
||||||
if prod_dir.exists():
|
if prod_dir.exists():
|
||||||
@@ -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"
|
return prod_dir / f"{chosen_sku}_已填写_{int(time.time())}.xlsx"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_fatal_50x(e) -> bool:
|
||||||
|
"""致命图像服务错误(503 / No available compatible accounts)→ 不重试,提前终止。"""
|
||||||
|
try:
|
||||||
|
from graph.pinterest_pipeline import PinterestPipeline
|
||||||
|
return PinterestPipeline.is_fatal_503(e)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _retry_image(fn, *args, attempts: int = 3, backoff=(5, 20, 40), **kwargs):
|
def _retry_image(fn, *args, attempts: int = 3, backoff=(5, 20, 40), **kwargs):
|
||||||
"""图像合成带退避重试(网关超载/超时常见):成功返回 out_path;全部失败返回 None。"""
|
"""图像合成带退避重试(网关超载/超时常见):成功返回 out_path;全部失败返回 None。
|
||||||
import time as _t
|
|
||||||
|
致命 503(账户不可用)重试无效 → 直接抛出,交由调用方终止任务。
|
||||||
|
"""
|
||||||
last = None
|
last = None
|
||||||
for i in range(attempts):
|
for i in range(attempts):
|
||||||
try:
|
try:
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
if _is_fatal_50x(e):
|
||||||
|
raise
|
||||||
last = e
|
last = e
|
||||||
if i < attempts - 1:
|
if i < attempts - 1:
|
||||||
_t.sleep(backoff[i])
|
time.sleep(backoff[i])
|
||||||
print(f"[product] 图像合成重试 {attempts} 次均失败: {last}")
|
print(f"[product] 图像合成重试 {attempts} 次均失败: {last}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -135,16 +144,31 @@ def _process_spu(
|
|||||||
db_path, basemap_root, material_root, category, prod_dir, brief, ib,
|
db_path, basemap_root, material_root, category, prod_dir, brief, ib,
|
||||||
spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="",
|
spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="",
|
||||||
img_code="", model_img=None, design_size="1024x1024", compose_size="1536x2048",
|
img_code="", model_img=None, design_size="1024x1024", compose_size="1536x2048",
|
||||||
|
on_503=None,
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
|
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
|
||||||
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
|
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
|
||||||
img_code: 货号(前缀+3位计数);本产品所有图片文件归入 prod_dir/{img_code}/ 子文件夹
|
img_code: 货号(前缀+3位计数);本产品所有图片文件归入 prod_dir/{img_code}/ 子文件夹
|
||||||
(按货号命名,包含该货号对应的所有图片)。
|
(按货号命名,包含该货号对应的所有图片)。
|
||||||
|
on_503: 致命图像服务错误(503/账户不可用)回调(供调用方提前终止任务)。
|
||||||
返回 result dict;内部异常已兜底,不中断。
|
返回 result dict;内部异常已兜底,不中断。
|
||||||
"""
|
"""
|
||||||
# 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里
|
# 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里
|
||||||
prod_dir = prod_dir / img_code
|
prod_dir = prod_dir / img_code
|
||||||
prod_dir.mkdir(parents=True, exist_ok=True)
|
prod_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _fatal(e) -> bool:
|
||||||
|
"""致命图像服务错误(503/账户不可用)→ 通知 on_503 并返回 True(调用方应立即终止)。"""
|
||||||
|
try:
|
||||||
|
from graph.pinterest_pipeline import PinterestPipeline
|
||||||
|
if PinterestPipeline.is_fatal_503(e):
|
||||||
|
if on_503 is not None:
|
||||||
|
on_503()
|
||||||
|
return True
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
colors = list_colors(db_path, spu["code"])
|
colors = list_colors(db_path, spu["code"])
|
||||||
valid_codes = {c["sku_code"] for c in colors}
|
valid_codes = {c["sku_code"] for c in colors}
|
||||||
if sku_code:
|
if sku_code:
|
||||||
@@ -220,6 +244,9 @@ def _process_spu(
|
|||||||
result["design_from"] = "product"
|
result["design_from"] = "product"
|
||||||
print(f"{tag} 纯印花设计稿已生成(product 节点): {design_path}")
|
print(f"{tag} 纯印花设计稿已生成(product 节点): {design_path}")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
if _fatal(e):
|
||||||
|
print(f"{tag} 图像服务 503,终止: {e}")
|
||||||
|
return None
|
||||||
errors.append({"node": "product", "type": type(e).__name__, "message": f"设计稿生成失败: {e}", "trace": ""})
|
errors.append({"node": "product", "type": type(e).__name__, "message": f"设计稿生成失败: {e}", "trace": ""})
|
||||||
print(f"{tag} 设计稿生成失败: {e}")
|
print(f"{tag} 设计稿生成失败: {e}")
|
||||||
|
|
||||||
@@ -262,7 +289,11 @@ def _process_spu(
|
|||||||
print(f"{tag} 三图模特合成图已生成(耗时 {int(time.time()-t0)}s): {composite_path}")
|
print(f"{tag} 三图模特合成图已生成(耗时 {int(time.time()-t0)}s): {composite_path}")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
# 合成失败 → 带退避重试(网关超载/超时常见,重试 3 次)
|
# 合成失败 → 带退避重试(网关超载/超时常见,重试 3 次)
|
||||||
|
if _fatal(e):
|
||||||
|
print(f"{tag} 图像服务 503,终止: {e}")
|
||||||
|
return None
|
||||||
print(f"{tag} 三图合成失败,退避重试…: {e}")
|
print(f"{tag} 三图合成失败,退避重试…: {e}")
|
||||||
|
try:
|
||||||
retried = _retry_image(ib.print, wear_prompt, str(model_img), composite_path,
|
retried = _retry_image(ib.print, wear_prompt, str(model_img), composite_path,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
extra_images=[design_path, str(basemap_img)], size=compose_size)
|
extra_images=[design_path, str(basemap_img)], size=compose_size)
|
||||||
@@ -273,6 +304,11 @@ def _process_spu(
|
|||||||
errors.append({"node": "product", "type": type(e).__name__, "message": f"模特合成失败(重试仍失败): {e}", "trace": ""})
|
errors.append({"node": "product", "type": type(e).__name__, "message": f"模特合成失败(重试仍失败): {e}", "trace": ""})
|
||||||
print(f"{tag} 三图合成重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}")
|
print(f"{tag} 三图合成重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}")
|
||||||
return None
|
return None
|
||||||
|
except Exception as e2: # noqa: BLE001
|
||||||
|
if _fatal(e2):
|
||||||
|
print(f"{tag} 图像服务 503(重试时),终止: {e2}")
|
||||||
|
return None
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
# mark=1 无模特图 → 统一只做三合一,不做印花+底图两图合成
|
# mark=1 无模特图 → 统一只做三合一,不做印花+底图两图合成
|
||||||
if int(spu.get("mark") or 0) == 1:
|
if int(spu.get("mark") or 0) == 1:
|
||||||
@@ -289,7 +325,11 @@ def _process_spu(
|
|||||||
result["printed_path"] = printed_path
|
result["printed_path"] = printed_path
|
||||||
print(f"{tag} 平铺服装图已生成(无模特,底图+印花): {printed_path}")
|
print(f"{tag} 平铺服装图已生成(无模特,底图+印花): {printed_path}")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
if _fatal(e):
|
||||||
|
print(f"{tag} 图像服务 503,终止: {e}")
|
||||||
|
return None
|
||||||
print(f"{tag} 平铺服装图失败,退避重试…: {e}")
|
print(f"{tag} 平铺服装图失败,退避重试…: {e}")
|
||||||
|
try:
|
||||||
retried = _retry_image(ib.print, flat_prompt, str(basemap_img), printed_path,
|
retried = _retry_image(ib.print, flat_prompt, str(basemap_img), printed_path,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
extra_images=[design_path], size=compose_size)
|
extra_images=[design_path], size=compose_size)
|
||||||
@@ -300,6 +340,11 @@ def _process_spu(
|
|||||||
errors.append({"node": "product", "type": type(e).__name__, "message": f"平铺服装图生成失败(重试仍失败): {e}", "trace": ""})
|
errors.append({"node": "product", "type": type(e).__name__, "message": f"平铺服装图生成失败(重试仍失败): {e}", "trace": ""})
|
||||||
print(f"{tag} 平铺服装图重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}")
|
print(f"{tag} 平铺服装图重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}")
|
||||||
return None
|
return None
|
||||||
|
except Exception as e2: # noqa: BLE001
|
||||||
|
if _fatal(e2):
|
||||||
|
print(f"{tag} 图像服务 503(重试时),终止: {e2}")
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
|
||||||
# 7.2) 多色:单 SPU 多色时每个颜色再执行一次三合一(用各自底图),轮播图按颜色路由
|
# 7.2) 多色:单 SPU 多色时每个颜色再执行一次三合一(用各自底图),轮播图按颜色路由
|
||||||
color_composites: List[Dict[str, Any]] = []
|
color_composites: List[Dict[str, Any]] = []
|
||||||
@@ -322,6 +367,9 @@ def _process_spu(
|
|||||||
color_composites.append({"sku_code": sc, "color": col, "composite_path": cp})
|
color_composites.append({"sku_code": sc, "color": col, "composite_path": cp})
|
||||||
print(f"{tag} 颜色 {sc}({col})三合一已生成: {cp}")
|
print(f"{tag} 颜色 {sc}({col})三合一已生成: {cp}")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
if _fatal(e):
|
||||||
|
print(f"{tag} 颜色 {sc} 三合一遇 503,终止: {e}")
|
||||||
|
return None
|
||||||
errors.append({"node": "product", "type": type(e).__name__,
|
errors.append({"node": "product", "type": type(e).__name__,
|
||||||
"message": f"颜色 {sc} 三合一失败: {e}", "trace": ""})
|
"message": f"颜色 {sc} 三合一失败: {e}", "trace": ""})
|
||||||
result["color_composites"] = color_composites
|
result["color_composites"] = color_composites
|
||||||
@@ -445,23 +493,25 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
prod_dir = output_dir / "product"
|
prod_dir = output_dir / "product"
|
||||||
prod_dir.mkdir(parents=True, exist_ok=True)
|
prod_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# 4.1) 任务级模特分配(material_library-<category>):
|
# 4.1) 任务级模特分配(按 spu.mark 映射模特目录 → 过滤非 3:4 图片 → 每个任务随机抽取):
|
||||||
# 一个 SPU 对应一个模特;SPU(不同款)数 > 模特数 → 从全部模特循环兜底(允许重复)
|
# 每个产品任务(含同一 SPU 的多个款)都独立随机抽一个模特,保证同款多产品模特不重复
|
||||||
model_assign: Dict[str, Any] = {}
|
model_assign: Dict[int, Any] = {}
|
||||||
_all_models: List[str] = []
|
|
||||||
try:
|
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
|
except Exception: # noqa: BLE001
|
||||||
_all_models = []
|
mark_map = {}
|
||||||
if _all_models:
|
|
||||||
seen_spu: Dict[str, str] = {}
|
|
||||||
for _i, (_spu, _skus, _tb) in enumerate(worklist):
|
for _i, (_spu, _skus, _tb) in enumerate(worklist):
|
||||||
code = _spu.get("code", "")
|
mark = str(_spu.get("mark") or "").strip() or "1"
|
||||||
if code not in seen_spu:
|
folder = mark_map.get(mark, category)
|
||||||
seen_spu[code] = _all_models[_i % len(_all_models)] # SPU>模特数 → 循环兜底
|
try:
|
||||||
model_assign[code] = seen_spu[code]
|
pool_imgs = find_model_images_for_mark(db_path, material_root, mark, folder)
|
||||||
print(f"[product] 任务级模特分配:{len(seen_spu)} 个 SPU,模特池 {len(_all_models)} 张"
|
except Exception: # noqa: BLE001
|
||||||
f"{'(SPU>模特,循环兜底)' if len(seen_spu) > len(_all_models) else ''}")
|
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),
|
# 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)
|
concurrency = int(pcfg.get("concurrency") or 0) or min(len(worklist), 5)
|
||||||
print(f"[product] 并发 {concurrency}(每 SPU 一线程,上限 {concurrency})处理 {len(worklist)} 个产品任务")
|
print(f"[product] 并发 {concurrency}(每 SPU 一线程,上限 {concurrency})处理 {len(worklist)} 个产品任务")
|
||||||
|
|
||||||
def _run_one(idx: int, spu, skus, tb):
|
_safe_errors = ThreadSafeErrors()
|
||||||
|
|
||||||
|
def _run_one(wi: int, spu, skus, tb):
|
||||||
"""并发执行单个产品:返回 (result or None, img_code)。失败由 _process_spu 内部兜底。"""
|
"""并发执行单个产品:返回 (result or None, img_code)。失败由 _process_spu 内部兜底。"""
|
||||||
|
idx = start_idx + wi # 实际货号序号(start_idx 起自动续号)
|
||||||
img_code = f"{prefix}{idx:03d}" # 货号:图片按此命名(DG000_design.png…)
|
img_code = f"{prefix}{idx:03d}" # 货号:图片按此命名(DG000_design.png…)
|
||||||
try:
|
try:
|
||||||
# 每个任务用自己的热点设计(designs_map),并拷贝为货号命名(designs/DG000_design.png)
|
# 每个任务用自己的热点设计(designs_map),并拷贝为货号命名(designs/DG000_design.png)
|
||||||
@@ -519,9 +572,9 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
tb = dict(tb)
|
tb = dict(tb)
|
||||||
tb["design_path"] = design_path
|
tb["design_path"] = design_path
|
||||||
r = _process_spu(db_path, basemap_root, material_root, category, prod_dir,
|
r = _process_spu(db_path, basemap_root, material_root, category, prod_dir,
|
||||||
tb, ib, spu, skus, pcfg, errors, design_path, title_backend,
|
tb, ib, spu, skus, pcfg, _safe_errors, design_path, title_backend,
|
||||||
country, img_code=img_code,
|
country, img_code=img_code,
|
||||||
model_img=model_assign.get(spu.get("code", "")),
|
model_img=model_assign.get(wi), # 按任务序号取独立随机模特
|
||||||
design_size=str((config.get("compose") or {}).get("design_size") or "1024x1024"),
|
design_size=str((config.get("compose") or {}).get("design_size") or "1024x1024"),
|
||||||
compose_size=str((config.get("compose") or {}).get("size") or "1536x2048"))
|
compose_size=str((config.get("compose") or {}).get("size") or "1536x2048"))
|
||||||
if r:
|
if r:
|
||||||
@@ -535,7 +588,7 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
# 货号自动续号:任务一开始全部按序分配(start_idx 起),不覆盖已生成的产物
|
# 货号自动续号:任务一开始全部按序分配(start_idx 起),不覆盖已生成的产物
|
||||||
start_idx = _next_img_idx(prod_dir, prefix)
|
start_idx = _next_img_idx(prod_dir, prefix)
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex:
|
||||||
futures = [ex.submit(_run_one, start_idx + i, spu, skus, tb)
|
futures = [ex.submit(_run_one, i, spu, skus, tb)
|
||||||
for i, (spu, skus, tb) in enumerate(worklist)]
|
for i, (spu, skus, tb) in enumerate(worklist)]
|
||||||
for f in concurrent.futures.as_completed(futures):
|
for f in concurrent.futures.as_completed(futures):
|
||||||
r, img_code = f.result()
|
r, img_code = f.result()
|
||||||
@@ -545,6 +598,9 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
_record_used(cache_dir, r) # (热点-风格) 去重记录 → 缓存根目录
|
_record_used(cache_dir, r) # (热点-风格) 去重记录 → 缓存根目录
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
|
# 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加)
|
||||||
|
if len(_safe_errors):
|
||||||
|
errors.extend(list(_safe_errors))
|
||||||
|
|
||||||
results.sort(key=lambda x: x.get("img_code", "")) # 按货号排序,模板/清单顺序稳定
|
results.sort(key=lambda x: x.get("img_code", "")) # 按货号排序,模板/清单顺序稳定
|
||||||
_write_products(prod_dir, results)
|
_write_products(prod_dir, results)
|
||||||
|
|||||||
+38
-12
@@ -22,6 +22,27 @@ PINTEREST_PRINT_SUFFIX = (
|
|||||||
"no garment, no shirt, no model, no mannequin, no watermark"
|
"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,
|
# 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif,
|
||||||
# 避免 gpt-image 等内容策略频繁拦截导致"生图限制多")
|
# 避免 gpt-image 等内容策略频繁拦截导致"生图限制多")
|
||||||
_IMG_RISKY_SWAP = {
|
_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()
|
composition = (r.get("composition") or derive_composition(r["topic"], r.get("design_category"))).strip()
|
||||||
|
|
||||||
prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country)
|
prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country)
|
||||||
# Pinterest 简报:直接用 LLM 多模态对图片的描述拼接的 image_prompt(跳过四要素模板),
|
|
||||||
# 仅追加统一的「小印花 + 纯白底」约束段;wearable/composite 仍用模板装配。
|
|
||||||
llm_ip = (r.get("image_prompt") or "").strip()
|
llm_ip = (r.get("image_prompt") or "").strip()
|
||||||
|
llm_neg = (r.get("negative_prompt") or "").strip()
|
||||||
if r.get("source") == "pinterest" and llm_ip:
|
if r.get("source") == "pinterest" and llm_ip:
|
||||||
prompts["image_prompt"] = llm_ip + PINTEREST_PRINT_SUFFIX
|
# —— Pinterest 参考模式:跳过四要素模板,按 4 段结构拼 image_prompt ——
|
||||||
print(f"[prompt] Pinterest 简报用 LLM 多模态描述作为 image_prompt(跳过四要素模板): 「{r['topic']}」")
|
# ① image_prompt(分析模型产出) + 固定输出形态后缀 PINTEREST_PRINT_SUFFIX
|
||||||
# 文字印花(约 30% 概率):简报有 slogan 时,随机注入文字段到设计稿提示词
|
# ② 负向条款(由 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()
|
slogan = (r.get("slogan") or "").strip()
|
||||||
if slogan and random.random() < float(config.get("prompt_templates", {}).get("text_ratio", 0.3)):
|
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, '
|
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')
|
f'lettering clean and correctly spelled, high contrast, as the focal text of the print')
|
||||||
prompts["image_prompt"] = prompts["image_prompt"] + text_seg
|
prompts["image_prompt"] = prompts["image_prompt"] + text_seg
|
||||||
r["used_slogan"] = slogan
|
r["used_slogan"] = slogan
|
||||||
# review(疑似商标/受保护主题)→ 动态注入「原创化魔改」引导:只做风格参考,禁止复刻品牌/商标/角色,
|
# review(疑似商标/受保护主题)→ 追加「原创化魔改」引导(两个模式通用)
|
||||||
# 换名换细节,生成通用非侵权的致敬式设计
|
|
||||||
if str(r.get("risk_level", "")).strip().lower() == "review":
|
if str(r.get("risk_level", "")).strip().lower() == "review":
|
||||||
prompts["image_prompt"] = (prompts["image_prompt"]
|
prompts["image_prompt"] = prompts["image_prompt"] + " " + REVIEW_REBRAND_HINT
|
||||||
+ " IMPORTANT: this theme is ONLY a loose stylistic reference. "
|
|
||||||
"Do NOT reproduce any brand logo, trademark, character, mascot, copyrighted artwork or real person. "
|
|
||||||
"Create a fully ORIGINAL design with a different name and distinct visual details and colors — "
|
|
||||||
"a generic, non-infringing homage in the same mood, clearly distinct from the original.")
|
|
||||||
print(f"[prompt] review 简报注入原创化魔改引导: 「{r['topic']}」")
|
print(f"[prompt] review 简报注入原创化魔改引导: 「{r['topic']}」")
|
||||||
r.update(prompts)
|
r.update(prompts)
|
||||||
r["motif"] = motif
|
r["motif"] = motif
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ from graph.validate import with_fallback
|
|||||||
def _template_out_path(prod_dir: Path, tpl_name: str) -> Path:
|
def _template_out_path(prod_dir: Path, tpl_name: str) -> Path:
|
||||||
"""模板输出路径:默认 {tpl_name}_已填写.xlsx;已存在/被占用则自动换名加序号(同款号多产品不互相覆盖)。"""
|
"""模板输出路径:默认 {tpl_name}_已填写.xlsx;已存在/被占用则自动换名加序号(同款号多产品不互相覆盖)。"""
|
||||||
base = prod_dir / f"{tpl_name}_已填写.xlsx"
|
base = prod_dir / f"{tpl_name}_已填写.xlsx"
|
||||||
try:
|
|
||||||
with open(base, "ab"):
|
|
||||||
pass
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if not base.exists():
|
if not base.exists():
|
||||||
return base
|
return base
|
||||||
for i in range(2, 100):
|
for i in range(2, 100):
|
||||||
@@ -95,6 +89,7 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
sku_codes = [r.get("sku_code") or ""]
|
sku_codes = [r.get("sku_code") or ""]
|
||||||
batch.append({
|
batch.append({
|
||||||
"spu_code": r.get("spu_code", ""),
|
"spu_code": r.get("spu_code", ""),
|
||||||
|
"img_code": r.get("img_code", ""),
|
||||||
"sku_codes": sku_codes,
|
"sku_codes": sku_codes,
|
||||||
"images": [],
|
"images": [],
|
||||||
"spu_per_color": True, # 每颜色一个独立 SPU 块(单色多 SPU)
|
"spu_per_color": True, # 每颜色一个独立 SPU 块(单色多 SPU)
|
||||||
@@ -107,13 +102,24 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"seed_shot_urls": r.get("seed_shot_urls") or [],
|
"seed_shot_urls": r.get("seed_shot_urls") or [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 写入模板前按货号(前端自定义前缀+3位计数,如 DG001)最后3位从小到大排序,按顺序插入
|
||||||
|
def _tail_num(rec: Dict[str, Any]) -> tuple:
|
||||||
|
code = str(rec.get("img_code") or rec.get("oss_code") or "")
|
||||||
|
try:
|
||||||
|
return (0, int(code[-3:]))
|
||||||
|
except ValueError:
|
||||||
|
return (1, 0)
|
||||||
|
batch.sort(key=_tail_num)
|
||||||
|
|
||||||
exported: List[str] = []
|
exported: List[str] = []
|
||||||
if batch:
|
if batch:
|
||||||
out = _template_out_path(prod_dir, "商品上传")
|
# 输出文件名 = 模板原文件名 + _已填写(如 NEW-波兰男黑T恤_已填写.xlsx)
|
||||||
|
out = _template_out_path(prod_dir, Path(tp).stem)
|
||||||
try:
|
try:
|
||||||
out = export_products(
|
out = export_products(
|
||||||
db_path, batch, tdir, tp, str(out),
|
db_path, batch, tdir, tp, str(out),
|
||||||
markup_percent=float(pcfg.get("markup_percent") or 0),
|
markup_percent=float(pcfg.get("markup_percent") or 0),
|
||||||
|
suggested_price_ratio=float(pcfg.get("suggested_price_ratio") or 0),
|
||||||
)
|
)
|
||||||
for r in products:
|
for r in products:
|
||||||
if (r.get("composite_path") or r.get("printed_path")) and (r.get("en_title") or "").strip():
|
if (r.get("composite_path") or r.get("printed_path")) and (r.get("en_title") or "").strip():
|
||||||
|
|||||||
@@ -88,10 +88,3 @@ def build_oss_key(country: str, timestamp: str, code: str, rand4: str, ext: str
|
|||||||
"""对象 key:{国家}/{时间戳}/{货号}_{4位随机}.jpg(如 GB/20260820150945/DG000_Ab3x.jpg)。"""
|
"""对象 key:{国家}/{时间戳}/{货号}_{4位随机}.jpg(如 GB/20260820150945/DG000_Ab3x.jpg)。"""
|
||||||
safe = lambda s: "".join(c for c in (s or "") if c.isalnum() or c in "-_").strip()
|
safe = lambda s: "".join(c for c in (s or "") if c.isalnum() or c in "-_").strip()
|
||||||
return f"{safe(country)}/{safe(timestamp)}/{safe(code)}_{safe(rand4)}{ext}"
|
return f"{safe(country)}/{safe(timestamp)}/{safe(code)}_{safe(rand4)}{ext}"
|
||||||
|
|
||||||
|
|
||||||
def random_code4() -> str:
|
|
||||||
"""4 位随机:大小写英文 + 数字。"""
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
return "".join(random.choices(string.ascii_letters + string.digits, k=4))
|
|
||||||
|
|||||||
+186
-18
@@ -12,6 +12,7 @@
|
|||||||
并发上限与 product_node 一致(默认 5),避免压垮图像网关。
|
并发上限与 product_node 一致(默认 5),避免压垮图像网关。
|
||||||
"""
|
"""
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -35,13 +36,23 @@ class PinterestPipeline:
|
|||||||
self._briefs: List[Dict[str, Any]] = []
|
self._briefs: List[Dict[str, Any]] = []
|
||||||
self._done = False
|
self._done = False
|
||||||
self._cursor = 0
|
self._cursor = 0
|
||||||
|
# 正在处理(已提交线程池、尚未完成)的简报数,供路由判断简报池是否空闲
|
||||||
|
self._in_flight = 0
|
||||||
|
|
||||||
# 结果
|
# 结果:每完成一个产品立即落盘追加写入 products_pending.jsonl,
|
||||||
|
# 内存列表仅作缓存(finish 时再读盘合并),中途崩溃也不丢已完成产品。
|
||||||
self._products: List[Dict[str, Any]] = []
|
self._products: List[Dict[str, Any]] = []
|
||||||
self._products_lock = threading.Lock()
|
self._products_lock = threading.Lock()
|
||||||
self._errors: List[Dict[str, Any]] = []
|
self._errors: List[Dict[str, Any]] = []
|
||||||
self._errors_lock = threading.Lock()
|
self._errors_lock = threading.Lock()
|
||||||
|
|
||||||
|
# 致命图像服务错误(53/账户不可用):置位后终止分发、丢弃未完成简报,仅保留已完成产品
|
||||||
|
self._fatal_lock = threading.Lock()
|
||||||
|
self._fatal_503 = False
|
||||||
|
|
||||||
|
# 已完成产品落盘文件(JSONL 追加写):output/<country>/<ts>/products_pending.jsonl
|
||||||
|
self._pending_file = self.output_dir / "products_pending.jsonl"
|
||||||
|
|
||||||
# 路径解析(复用 product_node 的 _abs 逻辑:运行根优先,其次数据根)
|
# 路径解析(复用 product_node 的 _abs 逻辑:运行根优先,其次数据根)
|
||||||
pcfg = self.config.get("product") or {}
|
pcfg = self.config.get("product") or {}
|
||||||
|
|
||||||
@@ -188,19 +199,28 @@ class PinterestPipeline:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _assign_models(self) -> Dict[str, Any]:
|
def _assign_models(self) -> Dict[str, Any]:
|
||||||
|
"""任务级模特分配:按 spu.mark 映射模特目录 → 过滤非 3:4 图片 → 每个任务随机抽取。
|
||||||
|
|
||||||
|
每个产品任务(含同一 SPU 的多个款)都独立随机抽一个模特,保证同款多产品模特不重复。
|
||||||
|
"""
|
||||||
model_assign: Dict[str, Any] = {}
|
model_assign: Dict[str, Any] = {}
|
||||||
try:
|
try:
|
||||||
from graph.product import find_first_model_folder
|
from graph.product import build_mark_model_map, find_model_images_for_mark
|
||||||
_folder, _all_models = find_first_model_folder(self._material_root, self._category)
|
mark_map = build_mark_model_map(self._db_path, self._material_root)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
_all_models = []
|
mark_map = {}
|
||||||
if _all_models:
|
# 每个任务按序号绑定独立随机模特(同 SPU 多款也各自随机,不共用)
|
||||||
seen: Dict[str, str] = {}
|
|
||||||
for _i, (spu, _skus) in enumerate(self._worklist):
|
for _i, (spu, _skus) in enumerate(self._worklist):
|
||||||
code = spu.get("code", "")
|
key = f"task_{_i}"
|
||||||
if code not in seen:
|
mark = str(spu.get("mark") or "").strip() or "1"
|
||||||
seen[code] = _all_models[_i % len(_all_models)]
|
folder = mark_map.get(mark, self._category)
|
||||||
model_assign[code] = seen[code]
|
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
|
return model_assign
|
||||||
|
|
||||||
def _load_materials(self) -> Dict[str, str]:
|
def _load_materials(self) -> Dict[str, str]:
|
||||||
@@ -237,6 +257,48 @@ class PinterestPipeline:
|
|||||||
with self._err400_lock:
|
with self._err400_lock:
|
||||||
return self._err400_aborted
|
return self._err400_aborted
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 致命图像服务错误(503 / 账户不可用):重试无效,提前终止整个任务
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@staticmethod
|
||||||
|
def is_fatal_503(exc) -> bool:
|
||||||
|
"""判断异常是否为「图像服务不可用」类致命错误(503 / No available compatible accounts)。
|
||||||
|
|
||||||
|
这类错误说明账户配额耗尽或网关故障,重试必然失败,应提前终止任务而非无意义重试。
|
||||||
|
"""
|
||||||
|
msg = str(exc)
|
||||||
|
if "503" in msg:
|
||||||
|
return True
|
||||||
|
low = msg.lower()
|
||||||
|
return "no available compatible accounts" in low or "account" in low and "not available" in low
|
||||||
|
|
||||||
|
def record_503(self) -> bool:
|
||||||
|
"""记录一次致命 503:首次触发即置位终止标志(后续请求直接短路不再提交)。
|
||||||
|
|
||||||
|
返回 True 表示本次触发终止(调用方应立即停止当前链路)。
|
||||||
|
"""
|
||||||
|
with self._fatal_lock:
|
||||||
|
first = not self._fatal_503
|
||||||
|
self._fatal_503 = True
|
||||||
|
if first:
|
||||||
|
print("[pinterest_pipeline] ⛔ 检测到图像服务 503(No available compatible accounts),"
|
||||||
|
"重试无效 → 提前终止任务,未完成产品将废弃,仅保留已完成产品")
|
||||||
|
return first
|
||||||
|
|
||||||
|
def is_fatal_503_aborted(self) -> bool:
|
||||||
|
with self._fatal_lock:
|
||||||
|
return self._fatal_503
|
||||||
|
|
||||||
|
def abort_unfinished(self) -> None:
|
||||||
|
"""终止分发:丢弃简报池中所有未完成简报(已完成的落盘产品保留)。"""
|
||||||
|
with self._cond:
|
||||||
|
dropped = len(self._briefs)
|
||||||
|
self._briefs = []
|
||||||
|
self._done = True
|
||||||
|
self._cond.notify_all()
|
||||||
|
if dropped:
|
||||||
|
print(f"[pinterest_pipeline] 503 终止:丢弃未完成简报 {dropped} 条(未完成产品废弃)")
|
||||||
|
|
||||||
def _abort_current_term(self) -> None:
|
def _abort_current_term(self) -> None:
|
||||||
"""放弃当前种子词:清空其未完成简报 + 图池未消费图片(已完成的保留)。"""
|
"""放弃当前种子词:清空其未完成简报 + 图池未消费图片(已完成的保留)。"""
|
||||||
term = self._err400_term
|
term = self._err400_term
|
||||||
@@ -298,18 +360,52 @@ class PinterestPipeline:
|
|||||||
self._cond.notify_all()
|
self._cond.notify_all()
|
||||||
print(f"[pinterest_pipeline] 简报池 +{len(briefs)} 条(待处理 {len(self._briefs)})")
|
print(f"[pinterest_pipeline] 简报池 +{len(briefs)} 条(待处理 {len(self._briefs)})")
|
||||||
|
|
||||||
|
def pending_count(self) -> int:
|
||||||
|
"""简报池中待处理 + 正在处理的简报数(供路由判断是否需要补图/补分析)。"""
|
||||||
|
with self._cond:
|
||||||
|
queued = len(self._briefs)
|
||||||
|
return queued + self._in_flight
|
||||||
|
|
||||||
|
def wait_idle(self, timeout: Optional[float] = None) -> bool:
|
||||||
|
"""阻塞等待简报池消化完(无待处理且无在途),返回是否已空闲。
|
||||||
|
|
||||||
|
用 Condition 等待(_process_one 完成时 notify_all 唤醒),而非轮询 sleep,
|
||||||
|
避免 wait 循环疯狂刷屏。timeout 为 None 时无限等待(受 _done 保护)。
|
||||||
|
"""
|
||||||
|
with self._cond:
|
||||||
|
while (self._briefs or self._in_flight > 0) and not self._done:
|
||||||
|
if timeout is not None:
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
remaining = deadline - time.time()
|
||||||
|
if remaining <= 0:
|
||||||
|
return False
|
||||||
|
self._cond.wait(min(remaining, 1.0))
|
||||||
|
else:
|
||||||
|
self._cond.wait()
|
||||||
|
return not self._briefs and self._in_flight <= 0
|
||||||
|
|
||||||
def finish(self) -> tuple:
|
def finish(self) -> tuple:
|
||||||
"""排空简报池、等待全部产品完成,返回 (products, errors)。"""
|
"""排空简报池、等待全部产品完成,返回 (products, errors)。
|
||||||
|
|
||||||
|
产品来源:手动已完成(内存缓存)+ 落盘文件(products_pending.jsonl)
|
||||||
|
按货号去重合并——即使中途 503 终止/崩溃,已完成产品也不丢。
|
||||||
|
"""
|
||||||
with self._cond:
|
with self._cond:
|
||||||
self._done = True
|
self._done = True
|
||||||
self._cond.notify_all()
|
self._cond.notify_all()
|
||||||
self._dispatcher.join()
|
self._dispatcher.join()
|
||||||
self._pool.shutdown(wait=True)
|
self._pool.shutdown(wait=True)
|
||||||
|
# 读盘 + 内存合并去重(内存为准,但以落盘为最终权威——崩溃恢复后走落盘)
|
||||||
|
pending = self.load_pending()
|
||||||
|
merged = {str(p.get("img_code", "")): p for p in pending}
|
||||||
with self._products_lock:
|
with self._products_lock:
|
||||||
products = list(self._products)
|
for p in self._products:
|
||||||
|
merged[str(p.get("img_code", ""))] = p
|
||||||
|
products = [merged[k] for k in merged if k]
|
||||||
with self._errors_lock:
|
with self._errors_lock:
|
||||||
errors = list(self._errors)
|
errors = list(self._errors)
|
||||||
print(f"[pinterest_pipeline] 收尾:完成 {len(products)} 个产品,错误 {len(errors)}")
|
print(f"[pinterest_pipeline] 收尾:完成 {len(products)} 个产品,错误 {len(errors)}"
|
||||||
|
f"{'(含落盘恢复 ' + str(len(pending)) + ')' if pending else ''}")
|
||||||
return products, errors
|
return products, errors
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -322,8 +418,16 @@ class PinterestPipeline:
|
|||||||
self._cond.wait()
|
self._cond.wait()
|
||||||
if self._done and not self._briefs:
|
if self._done and not self._briefs:
|
||||||
break
|
break
|
||||||
|
if self._fatal_503:
|
||||||
|
# 致命 503:不再分发新简报(未完成的废弃,仅保留已完成落盘产品)
|
||||||
|
self._briefs = []
|
||||||
|
self._done = True
|
||||||
|
self._cond.notify_all()
|
||||||
|
break
|
||||||
batch = self._briefs
|
batch = self._briefs
|
||||||
self._briefs = []
|
self._briefs = []
|
||||||
|
# 与 _briefs 清空同一临界区递增在途数,避免 wait_idle 误判空闲
|
||||||
|
self._in_flight += len(batch)
|
||||||
for b in batch:
|
for b in batch:
|
||||||
with self._cond:
|
with self._cond:
|
||||||
idx = self._cursor
|
idx = self._cursor
|
||||||
@@ -335,6 +439,8 @@ class PinterestPipeline:
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _process_one(self, brief: Dict[str, Any], idx: int) -> None:
|
def _process_one(self, brief: Dict[str, Any], idx: int) -> None:
|
||||||
try:
|
try:
|
||||||
|
if self.is_fatal_503_aborted():
|
||||||
|
return
|
||||||
# 0) 先定货号:整条链路(设计/三合一/种草图)都用它命名与匹配,避免序号错位
|
# 0) 先定货号:整条链路(设计/三合一/种草图)都用它命名与匹配,避免序号错位
|
||||||
if idx >= len(self._worklist):
|
if idx >= len(self._worklist):
|
||||||
print(f"[pinterest_pipeline] 简报 {idx} 无对应产品任务,跳过")
|
print(f"[pinterest_pipeline] 简报 {idx} 无对应产品任务,跳过")
|
||||||
@@ -343,6 +449,8 @@ class PinterestPipeline:
|
|||||||
img_code = f"{self._prefix}{idx:03d}"
|
img_code = f"{self._prefix}{idx:03d}"
|
||||||
# 1) 生成设计(compose)——直接按货号命名 designs/{img_code}_design.png
|
# 1) 生成设计(compose)——直接按货号命名 designs/{img_code}_design.png
|
||||||
design_path = self._gen_design(brief, img_code)
|
design_path = self._gen_design(brief, img_code)
|
||||||
|
if self.is_fatal_503_aborted():
|
||||||
|
return
|
||||||
if self.is_400_aborted():
|
if self.is_400_aborted():
|
||||||
# 当前种子词 400 超限已放弃:正在生成的当个也放弃,不进入后续链路
|
# 当前种子词 400 超限已放弃:正在生成的当个也放弃,不进入后续链路
|
||||||
print(f"[pinterest_pipeline] 当前种子词 400 超限已放弃,跳过简报 {idx}")
|
print(f"[pinterest_pipeline] 当前种子词 400 超限已放弃,跳过简报 {idx}")
|
||||||
@@ -357,13 +465,17 @@ class PinterestPipeline:
|
|||||||
return
|
return
|
||||||
brief = new_brief
|
brief = new_brief
|
||||||
design_path = self._gen_design(brief, img_code)
|
design_path = self._gen_design(brief, img_code)
|
||||||
|
if self.is_fatal_503_aborted():
|
||||||
|
return
|
||||||
if design_path:
|
if design_path:
|
||||||
break
|
break
|
||||||
if not design_path:
|
if not design_path:
|
||||||
return
|
return
|
||||||
brief["design_path"] = design_path
|
brief["design_path"] = design_path
|
||||||
# 2) 三合一(product)——同一货号
|
# 2) 三合一(product)——同一货号;task_idx=本次简报序号,模特按任务独立随机
|
||||||
prod = self._process_spu(brief, spu, skus, img_code, design_path)
|
prod = self._process_spu(brief, spu, skus, img_code, design_path, task_idx=idx)
|
||||||
|
if self.is_fatal_503_aborted():
|
||||||
|
return
|
||||||
if not prod:
|
if not prod:
|
||||||
return
|
return
|
||||||
# 3) OSS 上传
|
# 3) OSS 上传
|
||||||
@@ -376,6 +488,8 @@ class PinterestPipeline:
|
|||||||
_record_used(self.cache_dir, prod)
|
_record_used(self.cache_dir, prod)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
|
# 5) 落盘:每完成一个产品立即追加写入 products_pending.jsonl(不依赖内存,崩溃不丢)
|
||||||
|
self._persist_product(prod)
|
||||||
with self._products_lock:
|
with self._products_lock:
|
||||||
self._products.append(prod)
|
self._products.append(prod)
|
||||||
print(f"[pinterest_pipeline] 产品完成: {prod.get('img_code', '')}"
|
print(f"[pinterest_pipeline] 产品完成: {prod.get('img_code', '')}"
|
||||||
@@ -385,6 +499,42 @@ class PinterestPipeline:
|
|||||||
self._errors.append({"node": "pinterest_pipeline", "type": type(e).__name__,
|
self._errors.append({"node": "pinterest_pipeline", "type": type(e).__name__,
|
||||||
"message": f"简报 {idx} 处理失败: {e}", "trace": ""})
|
"message": f"简报 {idx} 处理失败: {e}", "trace": ""})
|
||||||
print(f"[pinterest_pipeline] 简报 {idx} 处理失败: {e}")
|
print(f"[pinterest_pipeline] 简报 {idx} 处理失败: {e}")
|
||||||
|
finally:
|
||||||
|
with self._cond:
|
||||||
|
self._in_flight = max(0, self._in_flight - 1)
|
||||||
|
self._cond.notify_all()
|
||||||
|
|
||||||
|
def _persist_product(self, prod: Dict[str, Any]) -> None:
|
||||||
|
"""把已完成产品追加写入 products_pending.jsonl(JSONL 每行一个产品)。
|
||||||
|
|
||||||
|
落盘失败不阻塞主流程(仅告警);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]:
|
def _gen_design(self, brief: Dict[str, Any], img_code: str) -> Optional[str]:
|
||||||
if self._ib is None:
|
if self._ib is None:
|
||||||
@@ -398,10 +548,18 @@ class PinterestPipeline:
|
|||||||
if self.record_400():
|
if self.record_400():
|
||||||
self._abort_current_term()
|
self._abort_current_term()
|
||||||
|
|
||||||
|
def _on_503():
|
||||||
|
self.record_503()
|
||||||
|
self.abort_unfinished()
|
||||||
|
|
||||||
return generate_design(self._ib, brief, design_dir, img_code, self._errors,
|
return generate_design(self._ib, brief, design_dir, img_code, self._errors,
|
||||||
on_400=_on_400,
|
on_400=_on_400, on_503=_on_503,
|
||||||
size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024"))
|
size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024"))
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
if self.is_fatal_503(e):
|
||||||
|
self.record_503()
|
||||||
|
self.abort_unfinished()
|
||||||
|
return None
|
||||||
with self._errors_lock:
|
with self._errors_lock:
|
||||||
self._errors.append({"node": "compose", "type": type(e).__name__,
|
self._errors.append({"node": "compose", "type": type(e).__name__,
|
||||||
"message": f"设计稿生成失败 {brief.get('topic', '')}: {e}",
|
"message": f"设计稿生成失败 {brief.get('topic', '')}: {e}",
|
||||||
@@ -473,7 +631,7 @@ class PinterestPipeline:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _process_spu(self, brief: Dict[str, Any], spu, skus: str, img_code: str,
|
def _process_spu(self, brief: Dict[str, Any], spu, skus: str, img_code: str,
|
||||||
design_path: str) -> Optional[Dict[str, Any]]:
|
design_path: str, task_idx: int = 0) -> Optional[Dict[str, Any]]:
|
||||||
from graph.nodes.product_node import _process_spu as _ps
|
from graph.nodes.product_node import _process_spu as _ps
|
||||||
prod_dir = self.output_dir / "product"
|
prod_dir = self.output_dir / "product"
|
||||||
prod_dir.mkdir(parents=True, exist_ok=True)
|
prod_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -483,7 +641,9 @@ class PinterestPipeline:
|
|||||||
r = _ps(self._db_path, self._basemap_root, self._material_root, self._category,
|
r = _ps(self._db_path, self._basemap_root, self._material_root, self._category,
|
||||||
prod_dir, brief, self._ib, spu, skus, self.config.get("product") or {},
|
prod_dir, brief, self._ib, spu, skus, self.config.get("product") or {},
|
||||||
self._errors, design_path, self._title_backend, self.country,
|
self._errors, design_path, self._title_backend, self.country,
|
||||||
img_code=img_code, model_img=self._model_assign.get(spu.get("code", "")))
|
img_code=img_code,
|
||||||
|
model_img=self._model_assign.get(f"task_{task_idx}"),
|
||||||
|
on_503=lambda: (self.record_503(), self.abort_unfinished()))
|
||||||
if r:
|
if r:
|
||||||
r["img_code"] = img_code
|
r["img_code"] = img_code
|
||||||
return r
|
return r
|
||||||
@@ -559,9 +719,17 @@ class PinterestPipeline:
|
|||||||
if not base or not Path(base).exists():
|
if not base or not Path(base).exists():
|
||||||
print(f"[pinterest_pipeline] {r.get('spu_code', '')} 参考图缺失,跳过该色种草图")
|
print(f"[pinterest_pipeline] {r.get('spu_code', '')} 参考图缺失,跳过该色种草图")
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir),
|
generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir),
|
||||||
r.get("composite_negative", ""),
|
r.get("composite_negative", ""),
|
||||||
size=size, prefix=pfx, gender=self._gender)
|
size=size, prefix=pfx, gender=self._gender)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
if self.is_fatal_503(e):
|
||||||
|
self.record_503()
|
||||||
|
self.abort_unfinished()
|
||||||
|
return
|
||||||
|
print(f"[pinterest_pipeline] 种草图生成失败(跳过该色): {e}")
|
||||||
|
continue
|
||||||
paths.extend(generated)
|
paths.extend(generated)
|
||||||
if not paths:
|
if not paths:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import sqlite3
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
# 支持的图片格式:模特图/底图均按此识别(png/jpg 等常见格式全覆盖;AVIF/GIF/TIFF 亦支持)
|
# 支持的图片格式:模特图/底图均按此识别(png/jpg 等常见格式全覆盖;AVIF/GIF/TIFF 亦支持)
|
||||||
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".avif", ".gif", ".tiff", ".tif"}
|
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".avif", ".gif", ".tiff", ".tif"}
|
||||||
|
|
||||||
@@ -114,6 +116,104 @@ def find_first_model_folder(material_root, preferred: Optional[str] = None):
|
|||||||
return None, []
|
return None, []
|
||||||
|
|
||||||
|
|
||||||
|
def image_ratio_ok(path, target_ratio: float = 3 / 4, tolerance: float = 0.06) -> bool:
|
||||||
|
"""图片宽高比是否接近目标比例(默认 3:4,相对容差 6%)。
|
||||||
|
|
||||||
|
相对容差:接受 [target*(1-tol), target*(1+tol)],对 3:4 即 0.705~0.795。
|
||||||
|
无法解析的图片(损坏/非标准)按不通过处理,避免坏图被当模特。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with Image.open(path) as im:
|
||||||
|
w, h = im.size
|
||||||
|
if w <= 0 or h <= 0:
|
||||||
|
return False
|
||||||
|
ratio = w / h
|
||||||
|
lo = target_ratio * (1 - tolerance)
|
||||||
|
hi = target_ratio * (1 + tolerance)
|
||||||
|
return lo <= ratio <= hi
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def 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]:
|
def first_available_sku(db_path, basemap_root, spu_code: str) -> Optional[str]:
|
||||||
"""返回该款号下第一个「本地有底图」的 SKU.code;无则 None。"""
|
"""返回该款号下第一个「本地有底图」的 SKU.code;无则 None。"""
|
||||||
for c in list_colors(db_path, spu_code):
|
for c in list_colors(db_path, spu_code):
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class AgentState(TypedDict, total=False):
|
|||||||
pinterest_search_terms: List[str] # pinterest_search 产出:LLM 生成的搜索词
|
pinterest_search_terms: List[str] # pinterest_search 产出:LLM 生成的搜索词
|
||||||
pinterest_images: Dict[str, List[str]] # pinterest_scrape 产出:搜索词 → 爬取图片路径列表
|
pinterest_images: Dict[str, List[str]] # pinterest_scrape 产出:搜索词 → 爬取图片路径列表
|
||||||
pinterest_briefs: List[Dict[str, Any]] # pinterest_analyze 产出:LLM 分析图片的原始设计简报
|
pinterest_briefs: List[Dict[str, Any]] # pinterest_analyze 产出:LLM 分析图片的原始设计简报
|
||||||
|
pinterest_login: Dict[str, Any] # pinterest_scrape 产出:登录态检测结果 {status, detail, ...}
|
||||||
|
|
||||||
# —— Pinterest 按需搜索循环状态 ——
|
# —— Pinterest 按需搜索循环状态 ——
|
||||||
pinterest_target: int # 目标简报数(= spu_tasks 数量,每款一个设计)
|
pinterest_target: int # 目标简报数(= spu_tasks 数量,每款一个设计)
|
||||||
|
|||||||
+110
-9
@@ -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:
|
def _size_rank(size) -> tuple:
|
||||||
"""把尺码字符串转成可排序 rank(从小到大)。
|
"""把尺码字符串转成可排序 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,
|
def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
|
||||||
color: Optional[str] = None,
|
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、风格=休闲、商品产地=经营站点;多颜色时用色值列区分)。
|
"""构造一行 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] = {
|
row: Dict[str, Any] = {
|
||||||
"基础信息-商品层级": "spu",
|
"基础信息-商品层级": "spu",
|
||||||
"SKC货号": spu_code, # code 路由为 SKC货号(用户要求)
|
"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
|
row["色值(主规格)"] = color
|
||||||
for dbk, header in SPU_MAP.items():
|
for dbk, header in SPU_MAP.items():
|
||||||
v = spu.get(dbk)
|
v = spu.get(dbk)
|
||||||
|
if dbk in ("component_1", "component_2", "component_3"):
|
||||||
|
v = _map_component(v, gender)
|
||||||
if v not in (None, ""):
|
if v not in (None, ""):
|
||||||
row[header] = v
|
row[header] = v
|
||||||
fabric = spu.get("fabric")
|
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)]
|
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,
|
def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color: str,
|
||||||
warehouses: List[str], markup_percent: float = 0.0,
|
warehouses: List[str], markup_percent: float = 0.0,
|
||||||
multi: bool = True, price_header: str = "申报价格-日本站",
|
multi: bool = True, price_header: str = "申报价格-日本站",
|
||||||
bust_headers: Optional[List[str]] = None,
|
bust_headers: Optional[List[str]] = None,
|
||||||
price_headers: Optional[List[str]] = None,
|
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)。
|
"""构造一行 SKU(固定字段:SPU货号、SKC货号=sku.code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。
|
||||||
价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100),
|
价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100),
|
||||||
预先填好。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。
|
预先填好。建议售价(suggested_price_headers 列,模板「建议售价」必填时才填)= 申报价格 × (1+suggested_price_ratio/100),
|
||||||
|
填了建议售价同时填「建议售价单位」=CNY。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。
|
||||||
size 填「尺码」列 + 所有「沙特阿拉伯码」列(sa_size_headers,如 尺码表-沙特阿拉伯码,检测到才填)。
|
size 填「尺码」列 + 所有「沙特阿拉伯码」列(sa_size_headers,如 尺码表-沙特阿拉伯码,检测到才填)。
|
||||||
规格类型2 统一填「尺码」两个字(不是 size 参数值)。"""
|
规格类型2 统一填「尺码」两个字(不是 size 参数值)。"""
|
||||||
row: Dict[str, Any] = {
|
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+加价%)
|
v = round(float(v) * (1 + markup_percent / 100), 2) # 申报价格 = price × (1+加价%)
|
||||||
for h in price_headers:
|
for h in price_headers:
|
||||||
row[h] = v
|
row[h] = v
|
||||||
|
# 建议售价 = 申报价格 × (1+建议售价比例%);模板「建议售价」必填时才填,填了同时填单位 CNY
|
||||||
|
if suggested_price_headers and suggested_price_ratio > 0:
|
||||||
|
suggested = round(v * (1 + suggested_price_ratio / 100), 2)
|
||||||
|
for h in suggested_price_headers:
|
||||||
|
if (suggested_required or {}).get(h, True):
|
||||||
|
row[h] = suggested
|
||||||
|
for uh in suggested_unit_headers:
|
||||||
|
row[uh] = "CNY"
|
||||||
continue
|
continue
|
||||||
if dbk == "size":
|
if dbk == "size":
|
||||||
if v in (None, ""):
|
if v in (None, ""):
|
||||||
@@ -445,6 +509,11 @@ def _insert_product_block(
|
|||||||
bust_headers: Optional[List[str]] = None,
|
bust_headers: Optional[List[str]] = None,
|
||||||
fabric_headers: Optional[List[str]] = None,
|
fabric_headers: Optional[List[str]] = None,
|
||||||
sa_size_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]:
|
) -> List[int]:
|
||||||
"""在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。
|
"""在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。
|
||||||
|
|
||||||
@@ -474,7 +543,8 @@ def _insert_product_block(
|
|||||||
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
|
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
|
||||||
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
|
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
|
||||||
block_rows.append(router.insert(
|
block_rows.append(router.insert(
|
||||||
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers),
|
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers,
|
||||||
|
gender=gender),
|
||||||
match="exact",
|
match="exact",
|
||||||
))
|
))
|
||||||
for ci, (sc, skus) in enumerate(skus_by_color):
|
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,
|
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
|
||||||
markup_percent=markup_percent, multi=True,
|
markup_percent=markup_percent, multi=True,
|
||||||
bust_headers=bust_headers, price_headers=price_headers,
|
bust_headers=bust_headers, price_headers=price_headers,
|
||||||
sa_size_headers=sa_size_headers),
|
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",
|
spu_code=spu_code, match="exact",
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -504,7 +578,8 @@ def _insert_product_block(
|
|||||||
else:
|
else:
|
||||||
# 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
|
# 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
|
||||||
block_rows.append(router.insert(
|
block_rows.append(router.insert(
|
||||||
_build_spu_row(spu, spu_code, origin_province, 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
|
multi_variant = len(skus_by_color) > 1
|
||||||
for ci, (sc, skus) in enumerate(skus_by_color):
|
for ci, (sc, skus) in enumerate(skus_by_color):
|
||||||
first = skus[0]
|
first = skus[0]
|
||||||
@@ -515,7 +590,11 @@ def _insert_product_block(
|
|||||||
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
|
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
|
||||||
markup_percent=markup_percent, multi=multi_variant,
|
markup_percent=markup_percent, multi=multi_variant,
|
||||||
bust_headers=bust_headers, price_headers=price_headers,
|
bust_headers=bust_headers, price_headers=price_headers,
|
||||||
sa_size_headers=sa_size_headers),
|
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 []
|
sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else []
|
||||||
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
|
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
|
||||||
@@ -554,6 +633,7 @@ def export_product(
|
|||||||
seed_shot_urls: Optional[List[str]] = None,
|
seed_shot_urls: Optional[List[str]] = None,
|
||||||
append_to: str = "",
|
append_to: str = "",
|
||||||
markup_percent: float = 0.0,
|
markup_percent: float = 0.0,
|
||||||
|
suggested_price_ratio: float = 0.0,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
|
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
|
||||||
|
|
||||||
@@ -584,6 +664,11 @@ def export_product(
|
|||||||
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
|
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
|
||||||
fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填)
|
fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填)
|
||||||
sa_size_headers = _find_sa_size_headers(router) # 沙特阿拉伯码列(尺码表-沙特阿拉伯码…检测到才填)
|
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,
|
_insert_product_block(router, db_path, spu_code, sku_code,
|
||||||
origin_province, warehouses, price_headers,
|
origin_province, warehouses, price_headers,
|
||||||
markup_percent=markup_percent, images=images,
|
markup_percent=markup_percent, images=images,
|
||||||
@@ -594,7 +679,12 @@ def export_product(
|
|||||||
seed_shot_urls=seed_shot_urls,
|
seed_shot_urls=seed_shot_urls,
|
||||||
bust_headers=bust_headers,
|
bust_headers=bust_headers,
|
||||||
fabric_headers=fabric_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)
|
out = router.save(out_path)
|
||||||
return Path(out)
|
return Path(out)
|
||||||
finally:
|
finally:
|
||||||
@@ -611,6 +701,7 @@ def export_products(
|
|||||||
template_path: str,
|
template_path: str,
|
||||||
out_path: str,
|
out_path: str,
|
||||||
markup_percent: float = 0.0,
|
markup_percent: float = 0.0,
|
||||||
|
suggested_price_ratio: float = 0.0,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
|
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
|
||||||
|
|
||||||
@@ -628,6 +719,11 @@ def export_products(
|
|||||||
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
|
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
|
||||||
fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填)
|
fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填)
|
||||||
sa_size_headers = _find_sa_size_headers(router) # 沙特阿拉伯码列(尺码表-沙特阿拉伯码…检测到才填)
|
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:
|
for r in products:
|
||||||
try:
|
try:
|
||||||
_insert_product_block(
|
_insert_product_block(
|
||||||
@@ -647,6 +743,11 @@ def export_products(
|
|||||||
bust_headers=bust_headers,
|
bust_headers=bust_headers,
|
||||||
fabric_headers=fabric_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,
|
||||||
)
|
)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}")
|
print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}")
|
||||||
|
|||||||
@@ -8,10 +8,34 @@
|
|||||||
剔除非法记录并记录原因,保证下游拿到的数据"形状正确"。
|
剔除非法记录并记录原因,保证下游拿到的数据"形状正确"。
|
||||||
"""
|
"""
|
||||||
import functools
|
import functools
|
||||||
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadSafeErrors:
|
||||||
|
"""线程安全的错误收集器:并发节点(compose/product 等)内 append 错误用。
|
||||||
|
|
||||||
|
避免多个 worker 线程直接写共享 list 造成竞态;主线程统一合并到 state['errors']。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._items: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def append(self, item: Dict[str, Any]) -> None:
|
||||||
|
with self._lock:
|
||||||
|
self._items.append(item)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
with self._lock:
|
||||||
|
return iter(list(self._items))
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
with self._lock:
|
||||||
|
return len(self._items)
|
||||||
|
|
||||||
|
|
||||||
def with_fallback(node_name: str):
|
def with_fallback(node_name: str):
|
||||||
"""装饰器:捕获节点异常,转为 state['errors'] 中的一条记录,返回空更新。
|
"""装饰器:捕获节点异常,转为 state['errors'] 中的一条记录,返回空更新。
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import os
|
|||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from typing import Any, Dict
|
||||||
from urllib.parse import urlparse, quote
|
from urllib.parse import urlparse, quote
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -218,6 +220,80 @@ def _session_user_data_dir() -> str:
|
|||||||
return ud
|
return ud
|
||||||
|
|
||||||
|
|
||||||
|
def check_login_state() -> Dict[str, Any]:
|
||||||
|
"""静态检测 Pinterest 登录态(不启动 Chrome,只读持久化 cookies 数据库)。
|
||||||
|
|
||||||
|
通过检查 .chrome_session/User Data 里的 Chrome cookies 数据库:
|
||||||
|
- 无目录 / 无 cookies 文件 → no_session(从未登录过)
|
||||||
|
- 有 cookies 但无 Pinterest 域 cookie → logged_out(未登录过 Pinterest)
|
||||||
|
- 有 Pinterest 域 cookie 但缺关键登录 cookie(_auth/csrftoken)或已过期 → logged_out
|
||||||
|
- 含 _auth 且未过期 → logged_in
|
||||||
|
- 数据库无法解析 → unknown(调用方走运行时检测兜底)
|
||||||
|
|
||||||
|
注意:Chrome 在 Windows 上对 cookie 值做 DPAPI 加密,这里只判断
|
||||||
|
cookie 的存在性 / 域名 / 过期时间,不解析密文值。
|
||||||
|
"""
|
||||||
|
ud = _session_user_data_dir()
|
||||||
|
out: Dict[str, Any] = {
|
||||||
|
"status": "unknown",
|
||||||
|
"session_dir": ud,
|
||||||
|
"cookies_file": "",
|
||||||
|
"pinterest_cookies": 0,
|
||||||
|
"has_auth_cookie": False,
|
||||||
|
"expired": False,
|
||||||
|
"detail": "",
|
||||||
|
}
|
||||||
|
if not os.path.isdir(ud):
|
||||||
|
out["status"] = "no_session"
|
||||||
|
out["detail"] = "未找到 Chrome 登录态目录(从未登录过 Pinterest)"
|
||||||
|
return out
|
||||||
|
cookies_file = None
|
||||||
|
for rel in ("Default/Network/Cookies", "Default/Cookies"):
|
||||||
|
cand = os.path.join(ud, rel)
|
||||||
|
if os.path.isfile(cand) and os.path.getsize(cand) > 0:
|
||||||
|
cookies_file = cand
|
||||||
|
break
|
||||||
|
if not cookies_file:
|
||||||
|
out["status"] = "no_session"
|
||||||
|
out["detail"] = "未找到 cookies 数据库(从未登录过 Pinterest)"
|
||||||
|
return out
|
||||||
|
out["cookies_file"] = cookies_file
|
||||||
|
try:
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect(f"file:{cookies_file}?mode=ro&immutable=1", uri=True)
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT host_key, name, expires_utc FROM cookies "
|
||||||
|
"WHERE host_key LIKE '%pinterest.com%' OR host_key LIKE '%pinimg.com%'"
|
||||||
|
).fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
out["detail"] = f"cookies 读取失败: {e}"
|
||||||
|
return out
|
||||||
|
out["pinterest_cookies"] = len(rows)
|
||||||
|
# WebKit epoch(1601-01-01 UTC)微秒偏移;expires_utc=0 表示会话 cookie(不判过期)
|
||||||
|
WEBKIT_EPOCH_US = 11644473600000000
|
||||||
|
now_us = int(time.time() * 1_000_000)
|
||||||
|
auth_names = ("_auth", "csrftoken", "sessionFunnelEventLogged")
|
||||||
|
for host, name, exp in rows:
|
||||||
|
if name in auth_names:
|
||||||
|
out["has_auth_cookie"] = True
|
||||||
|
if exp and exp != 0 and exp < now_us:
|
||||||
|
out["expired"] = True
|
||||||
|
if out["has_auth_cookie"] and not out["expired"]:
|
||||||
|
out["status"] = "logged_in"
|
||||||
|
out["detail"] = f"检测到 Pinterest 登录态({len(rows)} 个 Pinterest 域 cookie,含 _auth)"
|
||||||
|
elif out["pinterest_cookies"] > 0:
|
||||||
|
out["status"] = "logged_out"
|
||||||
|
out["detail"] = (f"存在 {len(rows)} 个 Pinterest 域 cookie,但缺少有效登录 cookie"
|
||||||
|
f"(可能已过期或退出登录)")
|
||||||
|
else:
|
||||||
|
out["status"] = "logged_out"
|
||||||
|
out["detail"] = "cookies 数据库中无 Pinterest 域 cookie(未登录过 Pinterest)"
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
async def download_image(session: aiohttp.ClientSession, url: str, idx: int,
|
async def download_image(session: aiohttp.ClientSession, url: str, idx: int,
|
||||||
sem: asyncio.Semaphore, save_dir: str, proxy: str | None):
|
sem: asyncio.Semaphore, save_dir: str, proxy: str | None):
|
||||||
filename = get_filename_from_url(url, idx)
|
filename = get_filename_from_url(url, idx)
|
||||||
@@ -284,7 +360,7 @@ async def human_scroll(page: Page, distance: int):
|
|||||||
await page.wait_for_timeout(random.randint(30, 100)) # 滚轮事件间隔
|
await page.wait_for_timeout(random.randint(30, 100)) # 滚轮事件间隔
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_logged_in(page: Page, search_url: str) -> bool:
|
async def _ensure_logged_in(page: Page, search_url: str, login_wait: bool = False) -> bool:
|
||||||
"""独立登录检查方法:在已跳到搜索页的前提下确认 Pinterest 已登录。
|
"""独立登录检查方法:在已跳到搜索页的前提下确认 Pinterest 已登录。
|
||||||
|
|
||||||
判定策略(避免误判):
|
判定策略(避免误判):
|
||||||
@@ -292,7 +368,9 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
|
|||||||
才认定未登录;否则默认已登录(不依赖可能不匹配的已登录选择器)。
|
才认定未登录;否则默认已登录(不依赖可能不匹配的已登录选择器)。
|
||||||
- Pinterest 是 SPA,goto 后需等待渲染,否则瞬间误判未登录。
|
- Pinterest 是 SPA,goto 后需等待渲染,否则瞬间误判未登录。
|
||||||
- 已登录 → 立即返回 True,走直路。
|
- 已登录 → 立即返回 True,走直路。
|
||||||
- 未登录(被弹回登录墙)→ 回退首页提示手动登录一次,登录成功后返回 True。
|
- 未登录:
|
||||||
|
login_wait=True → 回退首页提示手动登录一次,登录成功后返回 True;
|
||||||
|
login_wait=False → 不阻塞,直接返回 False(由调用方跳过并告警)。
|
||||||
- 一直未登录(用户关窗口/放弃)→ 返回 False。
|
- 一直未登录(用户关窗口/放弃)→ 返回 False。
|
||||||
"""
|
"""
|
||||||
async def _has_login_wall() -> bool:
|
async def _has_login_wall() -> bool:
|
||||||
@@ -327,6 +405,13 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
|
|||||||
print("✅ 已检测到登录态,直接开始搜索")
|
print("✅ 已检测到登录态,直接开始搜索")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# 未登录:默认不阻塞(跳过并告警);login_wait=True 才回退首页等手动登录
|
||||||
|
if not login_wait:
|
||||||
|
print("⚠️ 未检测到 Pinterest 登录态(cookie 缺失或已过期)。"
|
||||||
|
"请先在 pinterest_scraper/.chrome_session 目录登录 Pinterest 一次,"
|
||||||
|
"或设置 pinterest.login_wait=true 让程序自动等待手动登录。")
|
||||||
|
return False
|
||||||
|
|
||||||
# 未登录(搜索页被弹回登录墙):回退首页,提示手动登录一次,成功后立即继续
|
# 未登录(搜索页被弹回登录墙):回退首页,提示手动登录一次,成功后立即继续
|
||||||
print("⚠️ 当前未登录(搜索页被拦截)。请在弹出的浏览器窗口中手动登录,登录成功后将自动继续……")
|
print("⚠️ 当前未登录(搜索页被拦截)。请在弹出的浏览器窗口中手动登录,登录成功后将自动继续……")
|
||||||
try:
|
try:
|
||||||
@@ -351,7 +436,8 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool:
|
|||||||
|
|
||||||
async def scrape(keyword: str, count: int, headless: bool = False,
|
async def scrape(keyword: str, count: int, headless: bool = False,
|
||||||
proxy: str | None = None,
|
proxy: str | None = None,
|
||||||
cdp_url: str | None = None) -> set[str]:
|
cdp_url: str | None = None,
|
||||||
|
login_wait: bool = False) -> set[str]:
|
||||||
imgs_url: set[str] = set()
|
imgs_url: set[str] = set()
|
||||||
async with async_playwright() as p:
|
async with async_playwright() as p:
|
||||||
if cdp_url:
|
if cdp_url:
|
||||||
@@ -389,7 +475,7 @@ async def scrape(keyword: str, count: int, headless: bool = False,
|
|||||||
await page.goto(search_url, wait_until="domcontentloaded")
|
await page.goto(search_url, wait_until="domcontentloaded")
|
||||||
|
|
||||||
# 独立登录检查:已登录直接开始;未登录才回退首页等手动登录
|
# 独立登录检查:已登录直接开始;未登录才回退首页等手动登录
|
||||||
logged_in = await _ensure_logged_in(page, search_url)
|
logged_in = await _ensure_logged_in(page, search_url, login_wait=login_wait)
|
||||||
if not logged_in:
|
if not logged_in:
|
||||||
# 关闭浏览器(持久化目录已保存任何已有状态),中止本次爬取
|
# 关闭浏览器(持久化目录已保存任何已有状态),中止本次爬取
|
||||||
if cdp_url:
|
if cdp_url:
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ from pinterest_scraper.pinterest_image_capture import (
|
|||||||
|
|
||||||
def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = None,
|
def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = None,
|
||||||
proxy: Optional[str] = None, headless: bool = False,
|
proxy: Optional[str] = None, headless: bool = False,
|
||||||
cdp_url: Optional[str] = None) -> List[str]:
|
cdp_url: Optional[str] = None,
|
||||||
|
login_wait: bool = False) -> List[str]:
|
||||||
"""按关键词爬取 Pinterest 图片并下载到 save_dir,返回下载成功的文件路径列表。
|
"""按关键词爬取 Pinterest 图片并下载到 save_dir,返回下载成功的文件路径列表。
|
||||||
|
|
||||||
- proxy 为 None 时自动探测(环境变量/系统代理/本地常见端口);空字符串显式禁用。
|
- proxy 为 None 时自动探测(环境变量/系统代理/本地常见端口);空字符串显式禁用。
|
||||||
- 首次运行会弹出本地 Chrome 窗口,需手动登录 Pinterest 一次,登录态缓存在
|
- login_wait=False(默认):运行时检测到未登录时不阻塞,直接返回空列表(由调用方跳过并告警);
|
||||||
|
login_wait=True:弹出 Chrome 窗口等待手动登录一次,登录态缓存在
|
||||||
pinterest_scraper/.chrome_session,之后自动复用。
|
pinterest_scraper/.chrome_session,之后自动复用。
|
||||||
"""
|
"""
|
||||||
# 代理解析:显式传入 > 自动探测 > 直连
|
# 代理解析:显式传入 > 自动探测 > 直连
|
||||||
@@ -39,7 +41,7 @@ def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = No
|
|||||||
os.makedirs(out, exist_ok=True)
|
os.makedirs(out, exist_ok=True)
|
||||||
print(f"[pinterest] 关键词: {keyword} | 数量: {count} | 保存目录: {out}")
|
print(f"[pinterest] 关键词: {keyword} | 数量: {count} | 保存目录: {out}")
|
||||||
|
|
||||||
imgs_url = asyncio.run(scrape(keyword, count, headless, proxy, cdp_url))
|
imgs_url = asyncio.run(scrape(keyword, count, headless, proxy, cdp_url, login_wait))
|
||||||
if not imgs_url:
|
if not imgs_url:
|
||||||
print(f"[pinterest] 未收集到图片(可能未登录或搜索无结果): {keyword}")
|
print(f"[pinterest] 未收集到图片(可能未登录或搜索无结果): {keyword}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -406,7 +406,18 @@ class TemplateRouter:
|
|||||||
# 2) 路由到目标行
|
# 2) 路由到目标行
|
||||||
row = self.route(lv, spu, update=update, match=match)
|
row = self.route(lv, spu, update=update, match=match)
|
||||||
|
|
||||||
# 3) 写入
|
# 3) 清空目标行(仅新建行,update 覆盖模式保留模板原有内容):
|
||||||
|
# 模板数据区可能预填了示例/默认值(如 SKU销售信息-建议售价单位=CNY、
|
||||||
|
# SPU商品属性-印花类型=定位印花),不清理会把模板预填值误带到生成结果里。
|
||||||
|
# 只清空本行已写过的数据单元格,保留样式/数据验证下拉框。
|
||||||
|
if not update:
|
||||||
|
for c in range(1, self.ws.max_column + 1):
|
||||||
|
cell = self.ws.cell(row, c)
|
||||||
|
v = cell.value
|
||||||
|
if v is not None and str(v).strip() != "":
|
||||||
|
cell.value = None
|
||||||
|
|
||||||
|
# 4) 写入
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
# POD 趋势感知 Agent · 对接文档
|
|
||||||
|
|
||||||
> 面向开发者/维护者的适配与扩展指南。覆盖:项目架构、**如何适配新国家**、核心流程、缓存机制、合规红线、常见问题与打包发布。
|
|
||||||
> 适用版本:dist_v67 / 源码 `pod_trend_agent/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 项目概览
|
|
||||||
|
|
||||||
**目标**:为 POD(Print on Demand)出海业务自动捕捉海外热点 → 生成可打印的 T 恤印花设计 → 三图合成模特图 → 多语言标题 → 导出商品上传模板。
|
|
||||||
|
|
||||||
**技术栈**:Python 3.13 · LangGraph(7 节点流水线)· 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_trends(pytrends,429 兜底)/ 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: # LLM(screen/种子/标题共用)
|
|
||||||
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 sync(uv.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 验证。
|
|
||||||
Reference in New Issue
Block a user