From 2a96ec08701e9330cb04d5eda74531ef74fb1d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=BB=E6=B1=89=E7=86=99?= <3218485270@qq.com> Date: Fri, 28 Aug 2026 10:28:35 +0800 Subject: [PATCH] =?UTF-8?q?v88=20=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=EF=BC=9A=E4=BA=A7=E5=93=81=E8=90=BD=E7=9B=98=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=20+=20=E7=94=9F=E5=9B=BE=E7=BD=91=E5=85=B3=E9=80=82?= =?UTF-8?q?=E9=85=8D=20+=20=E6=A8=A1=E6=9D=BF=E5=AF=BC=E5=87=BA=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 产品持久化:每完成一个产品立即追加写入 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 排除测试产物 --- .gitignore | 4 + README.md | 28 +- config.yaml | 15 +- configs/model_features.yaml | 101 ++++--- configs/seed_shot_templates.yaml | 150 +++++----- db/spu_sku.db | Bin 1781760 -> 1781760 bytes graph/agent.py | 69 ++++- graph/backends/openai_image_backend.py | 79 ++++- graph/llms/mock_backend.py | 9 +- graph/llms/openai_compat_backend.py | 150 +++++----- graph/nodes/compose_node.py | 24 +- graph/nodes/fetch_node.py | 8 +- graph/nodes/filter_node.py | 14 +- graph/nodes/pinterest_analyze_node.py | 123 +++----- graph/nodes/pinterest_scrape_node.py | 34 ++- graph/nodes/product_node.py | 150 +++++++--- graph/nodes/prompt_node.py | 62 ++-- graph/nodes/template_export_node.py | 24 +- graph/oss_upload.py | 7 - graph/pinterest_pipeline.py | 212 +++++++++++-- graph/product.py | 100 +++++++ graph/state.py | 1 + graph/template_export.py | 119 +++++++- graph/validate.py | 24 ++ pinterest_scraper/pinterest_image_capture.py | 94 +++++- pinterest_scraper/scraper.py | 8 +- templates/template_router.py | 13 +- 对接文档.md | 294 ------------------- 28 files changed, 1187 insertions(+), 729 deletions(-) delete mode 100644 对接文档.md diff --git a/.gitignore b/.gitignore index 5686ee2..e9a06f7 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,10 @@ logs/ # Pinterest 登录态(首次手动登录后缓存,不入库) pinterest_scraper/.chrome_session/ +# 测试产物 +_probe_out/ +test_yunfei_concurrency.py + # 系统 .DS_Store Thumbs.db diff --git a/README.md b/README.md index 208bde5..0e8a71b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,9 @@ START → pinterest_init → pinterest_search → pinterest_scrape → pinterest **图池机制**:爬取的图片注册到 `image_pool.json`(路径+md5+搜索词);分析过的图片 md5 一律拉黑 (`used_images.json`,合适/不合适都拉黑);图池有未消费图片 → 直接分析(不搜索);不足 → 新一轮搜索。 每个简报带 `image_index`/`source_md5` 全局 id 校验,保证简报、设计稿、三合一套图对应同一张图。 +分析前对图片做文件完整性校验(`PIL.verify()` 过滤损坏/截断图),避免把坏 base64 发给多模态网关 +导致 400 拒收;分析失败(无有效图片/网关 400)时直接放弃该产品、跳过后续流程(**不做**纯文本降级、 +**不做** mock 兜底),由路由自动取新图继续。 **并发控制**(`config.yaml` 的 `pinterest:`):`analyze_concurrency`(分析线程数)、`analyze_batch` (每轮取图数,0=自动补齐让 pipeline 队列满并发)、`max_designs`(每搜索词简报上限)、 @@ -124,8 +127,26 @@ pod_trend_agent/ 设计稿生成后全局 MD5 去重(`.cache/global_design_md5.json`),跨国家生效。 5. **多模态分析**:大图先压缩(max 1024px / 1.5MB)再送 LLM,产出原创设计简报(motif/风格/配色/构图 + risk_level:safe/review/blocked)。blocked 拦截、review 加原创化魔改引导、safe 直接生成。 + 每张图由多模态模型判定 `suitable_for_print`(是否适合做印花),False 的简报在节点层被过滤丢弃, + 且该图 md5 立即拉黑,不进入生成流程。 6. **并发生成**:简报推入 `PinterestPipeline`,后台线程池(`product.concurrency`,默认 5)逐条 生成 设计稿 → 三合一 → OSS 上传 → 种草图,边分析边生成,不等全部分析完。 + 每完成一个产品立即追加落盘到 `output///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 信息(材质/成分/图案/领型/面料/克重/印花类型/尺码表/边长/包装重量…), -经 `templates/template_router.py` 的路由函数填入商品上传模板,输出 `_已填写.xlsx` 到 +经 `templates/template_router.py` 的路由函数填入商品上传模板,输出 `{模板原文件名}_已填写.xlsx` 到 `output/<国家>/product/`。模板已自包含在项目 `templates/`(`config.product.template_path`,可改为自由上传)。 - 数据从模板第 6 行(数据区空行)开始填,SPU 与各尺码 SKU 紧邻; +- **写入前按货号最后 3 位升序排序**:`template_export` 在批量填模板前取货号(`img_code`,前端 + `code_prefix` 前缀命名的货号)末 3 位数字从小到大排序再顺序插入,保证 SPU/SKU 块按货号有序, + 不受后台并发生成顺序影响; +- **不再产生空白 xlsx**:修复了旧版本探测模板可写性时误建空文件的 bug,product 目录只保留填好 + 的有效文件,文件名 = 模板原文件名 + `_已填写`(非固定「商品上传」); - **列名模糊匹配**:模板列名按名称匹配(非固定行列),支持中/英/日变体; - **胸围识别**:识别「基码表-胸围(cm)」「胸围全围(cm)」等含「胸围」的列,全部填 `sku.bust` 值; - **申报价格**:模糊匹配所有含「申报价格」的列,统一按加价后价格填写(`price × (1+markup_percent%)`); diff --git a/config.yaml b/config.yaml index a13a2cf..aacaba8 100644 --- a/config.yaml +++ b/config.yaml @@ -46,15 +46,18 @@ pinterest: seed_sample: 40 # 每次从国家种子池随机抽取多少个种子词给 LLM max_used_terms_in_prompt: 100 # 已用搜索词最多注入 LLM 提示词的个数(防 token 超限) images_per_term: 40 # 每个搜索词爬取图片数量 - analyze_per_term: 1 # 每次调用分析 1 张图(一张对应一个设计,一次 API 请求;简报带 image_index 全局 id 校验防错位) + analyze_per_term: 1 # 固定每次调用分析 1 张图(一次 API 请求一张,利于 LLM 注意力;简报带 brief_id uuid4 全局校验防错位) analyze_concurrency: 6 # 并发分析线程数(多并发提取简报) - analyze_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次补齐到目标所需/每词简报上限,让 pipeline 队列时刻满并发) + analyze_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次最多分析 max_designs 张;搜集完一批→生成一批简报,不够用了才主动采集) max_designs: 10 # 每个搜索词最多生成多少个设计简报 ref_images_per_design: 1 # 生图时每个设计附带几张爬取图作为参考(发给生图模型) supply_attempts: 3 # 设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数 err400_limit: 15 # 每个种子词累计 400(且错误含「内容/图片」)超限后放弃该种子词(多模态+生图合计) scrape_concurrency: 1 # 同时爬取几个搜索词(共享 .chrome_session 登录态,Chrome 单例,必须=1) headless: false # 爬取时是否无头(false=显示 Chrome 窗口,首次需手动登录) + proxy: "" # 图片下载代理(空=自动探测系统代理/VPN,默认走代理;配了如 http://127.0.0.1:7890 则用指定代理) + login_check: true # 爬取前静态检测 Pinterest 登录态(只读 .chrome_session cookies,不启动 Chrome;未登录则跳过本轮爬取并告警) + login_wait: false # 运行时检测到未登录时:false=跳过并告警(不阻塞);true=弹出 Chrome 等待手动登录 # 跨源融合权重(按 source 标签,无需和为 1) # 已下调 gt_trending(泛国家热点只作微弱信号),主力偏向 style+related(可印花型词)。 @@ -162,12 +165,12 @@ RULES:/n1.底色锁定:从图3提取衣服底色与面料,最终合成中必须1 # backend 留空则不生成设计稿(product 节点会自行回退生成)。 compose: backend: "openai" # 生成设计稿的图像后端:openai(需 api_key)/ mock(占位);留空=不生成 - api_key: "sk-ef1f251f059f9c2f8cc397de4a84e96d65775e1f2d32bb13b69b23dafc24d111" - base_url: "https://api.tofastcode.xyz" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404 + api_key: "sk-1CicJFACR4JCeLe6LTdsqSd0ffSrybMgmdfRiNfIlKok8MbS" + base_url: "https://img.yunfei.best/v1" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404 model: "gpt-image-2" size: "1536x2048" # 合成图/种草图等非设计图的统一尺寸(3:4) design_size: "1024x1024" # 纯印花设计稿尺寸(1:1) - background: "transparent" # 生成透明背景 PNG(gpt-image 系列支持);留空=默认背景 + execution_mode: "" # 网关异步/同步模式参数(仅 ai-media 等网关需要,如 "sync");yunfei 等标准 OpenAI 网关不认识该参数,留空=不传 seed: 0 # 随机种子:>0=固定(相同 seed 可复现,需网关支持);0/留空=每次随机 design_count: 1 # 用前 N 个 safe 简报生成设计稿(product 取第一个复用) design_workers: 5 # 设计稿并发生成线程数(仅热点采集模式 compose 节点用;Pinterest 模式走 pipeline 总并发 product.concurrency) @@ -208,9 +211,9 @@ product: sku_code: "" # 颜色编码(留空自动选该款第一个有底图的);多个颜色用逗号分隔(如 "DG015-VT01,DG015-DARK HEATHER") spu_per_color: # 留空自动判定:单色=每色一SPU;多色=单SPU多色(UI 不选择模板模式);CLI --single-spu 可显式覆盖 markup_percent: 0 # 加价百分比(按 SKU 最低价,供后续定价使用) + suggested_price_ratio: 30 # 建议售价比例(%):建议售价 = 申报价格 × (1+此比例/100);模板「建议售价」必填时才填 code_prefix: "DG" # 货号前缀(OSS 上传 key:{国家}/{时间戳}/{前缀+3位计数}_{4位随机}.jpg,000 起最多 999) backend: "mock" # openai / mock / 留空=跳过生成 - background: "transparent" # 设计稿透明背景(product 后端生图时也传 background=transparent) template_dir: "templates" # template_router.py 所在目录(项目内自包含) template_path: "templates/商品上传模版.xlsx" # 商品上传模板(可改为自由上传) concurrency: 5 # Pinterest 模式 pipeline 总并发(设计生成→三合一→种草图→标题 同一管道串行,总并发=此值) diff --git a/configs/model_features.yaml b/configs/model_features.yaml index 1515720..121514e 100644 --- a/configs/model_features.yaml +++ b/configs/model_features.yaml @@ -1,75 +1,88 @@ -# 模特特征库 —— 跨境电商基础款服装展示专用(已剔除外部穿搭与夸张妆造干扰) +# 模特特征库 —— 跨境电商年轻款服装展示专用(全库年龄20-33岁) # 占位符:[模特特征] 随机取用 -# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装。 -# 按性别分组:female=女模 / male=男模。 -# 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机。 +# 取用规则: +# 1. 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机。 +# 2. 如需同一商品多图保持同一模特(一景多动作),首次随机取到后请固定复用,不要每张图重新随机。 +# 3. 站点适配:北美/欧洲站优先「欧美系+多元族裔系」,东南亚站优先「东南亚裔」,日韩站优先「亚洲系」。 +# 描述规范:每条 = 年龄 + 族裔/风格标签 + 发型发色 + 肤色肤质 + 气质表情(字段顺序统一,出图更稳定)。 +# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装,不描述下半身。全库限定年轻人群。 model_features: female: - # ========== 女模 - 亚洲/清新/通勤类(适合基础款、纯色T恤) ========== - - "22岁亚洲清新女学生,通透伪素颜,黑直发,清瘦骨感" - - "25岁日韩系通勤女模,元气通透底妆,棕色微卷发,亲切感" - - "27岁亚洲极简风女模,冷白皮,无瑕光泽肌,利落黑色短发" - - "29岁亚洲轻熟女模,骨相美,微雾感底妆,气质温婉" + # ========== 1. 亚洲系(适合基础款/日韩风/通勤) ========== + - "22岁亚洲清新系女模,黑长直发,通透伪素颜,清瘦骨感,邻家亲和感" + - "25岁日韩系通勤女模,棕色微卷发,元气通透底妆,亲切微笑感" + - "27岁亚洲极简风女模,利落黑色短发,冷白皮,无瑕光泽肌,知性气质" + - "29岁亚洲轻熟女模,微卷深棕发,微雾感底妆,骨相精致,温婉气质" + - "31岁亚洲职场御姐女模,中分黑长发,干净底妆,干练自信气场" - # ========== 女模 - 欧美/阳光/甜心类(适合印花T恤、度假风) ========== - - "20岁欧美甜心女模,金发碧眼,阳光加州感,健康小麦色肌肤" - - "23岁法式慵懒女模,法式刘海,微卷棕发,通透红唇" - - "27岁欧美自然系女模,淡雀斑,大地色系淡妆,健康好气色" # 新增 - - "21岁迈阿密热辣女孩,金棕色大波浪,高光立体修容,健康古铜色肌肤" - - "24岁北美学院风女模,高马尾,通透元气妆,白皙肌肤" - - "34岁北美宝妈系女模,自然淡妆,栗色长发,亲切温暖的微笑" # 新增 + # ========== 2. 欧美阳光系(适合印花T恤/度假风) ========== + - "20岁欧美甜心女模,金色长直发,碧眼,健康小麦色肌肤,阳光加州感笑容" + - "23岁法式慵懒女模,法式刘海微卷棕发,通透底妆,柔和红唇,松弛气质" + - "27岁欧美自然系女模,浅棕长发,淡雀斑,大地色淡妆,健康好气色" + - "21岁迈阿密热辣女孩,金棕色大波浪,健康古铜色肌肤,立体修容,明媚笑容" + - "24岁北美学院风女模,高马尾,白皙肌肤,通透元气妆,青春活力感" + - "26岁北美邻家系女模,栗色长发,自然淡妆,亲切温暖笑容" - # ========== 女模 - 欧美/街头/个性类(适合街头风、美式复古印花) ========== - - "22岁纽约下城区酷女孩,挑染发色,清冷厌世感,苍白质肌肤" - - "28岁欧美高级中性模,凌乱短发,冷白皮,骨感清冷" - - "26岁拉美混血风女模,深邃眼窝,野生挑眉,健康小麦色肌肤" - - "25岁Y2K千禧辣妹,银色挑染发,清透裸妆,微胖立体感" + # ========== 3. 欧美街头系(适合美式复古/街头印花) ========== + - "22岁纽约下城区酷女孩,灰紫色挑染发,苍白冷感肌肤,酷感眼神" + - "28岁欧美高级中性风女模,凌乱金色短发,冷白皮,骨感清冷高级脸" + - "26岁拉美混血风女模,深棕长卷发,深邃眼窝,健康小麦色肌肤,野性眉形" + - "25岁Y2K千禧辣妹,银色挑染长发,清透裸妆,微胖立体感,个性张扬" - # ========== 女模 - 极简/性冷淡/高街类(适合纯色基础款、卫衣) ========== - - "30岁北欧极简风女模,冷白皮,无瑕哑光肌,银色利落短发" - - "29岁纽约高街潮流女模,冷感通透妆,深棕色直发,疏离眼神" + # ========== 4. 极简高街系(适合纯色基础款/卫衣) ========== + - "30岁北欧极简风女模,银色利落短发,冷白皮,无瑕哑光肌,疏离高级感" + - "29岁纽约高街潮流女模,深棕色直发,冷感通透妆,清冷眼神" - "31岁哥本哈根极简风设计师女模,利落黑色短发,黑白灰冷感气质" - - "28岁法式先锋艺术系女模,单边耳坠,冷感素颜,深金色发" + - "28岁法式先锋艺术系女模,深金色长发,冷感素颜,单边耳坠,艺术家气质" - # ========== 女模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备) ========== # 新增分组 + # ========== 5. 多元族裔系(北美/欧洲站覆盖) ========== - "26岁非裔自然卷女模,健康深色肌肤,光泽感底妆,明快笑容" - "29岁非裔都市丽人女模,利落编发,精致底妆,自信干练气场" - "24岁印度裔女模,浓密黑长发,深邃大眼,暖调蜜糖色肌肤" - "27岁拉丁裔阳光女模,栗色卷发,蜜色肌肤,热情爽朗笑容" - "32岁中东裔优雅女模,微卷黑发,轮廓深邃,端庄知性气质" - - "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容" # 大码:覆盖plus size市场 + - "25岁东南亚裔清甜女模,黑长直发,蜜糖色肌肤,元气亲和笑容" + + # ========== 6. 大码系(Plus Size市场) ========== + - "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容" + - "30岁欧美大码甜心女模,金色微卷发,健康白皙肌肤,自信明媚笑容" male: - # ========== 男模 - 亚洲/盐系/清新类(适合基础款、日韩系) ========== - - "23岁日系盐系男模,单眼皮,清瘦骨感,柔软黑发" - - "25岁亚洲清冷系男模,冷白皮,碎发自热感,下颌线清晰" + # ========== 1. 亚洲系(适合基础款/日韩风) ========== + - "23岁日系盐系男模,柔软黑发,单眼皮,清瘦骨感,干净少年感" + - "25岁亚洲清冷系男模,碎发自然感,冷白皮,下颌线清晰,清冷气质" - "28岁亚洲轻熟男模,干净利落的短发,健康肤色,稳重气质" - - "21岁亚洲阳光男大,通透底妆,微卷黑发,清爽笑容" + - "21岁亚洲阳光系男模,微卷黑发,通透底妆,清爽笑容" + - "26岁亚洲运动系男模,利落黑色短发,健康小麦色肌肤,阳光笑容" - # ========== 男模 - 欧美/街头/运动类(适合美式复古、街头印花) ========== + # ========== 2. 欧美街头/运动系(适合美式复古/街头印花) ========== - "20岁加州冲浪系男模,阳光金发,麦色健康肌,清爽笑容" - "24岁欧美健身教练型男,肌肉线条分明,小麦色肌肤,硬朗骨相" - - "22岁纽约街头滑板男模,脏辫,清冷厌世感,苍白肌肤" - - "27岁美式机车硬汉男模,粗犷髂廓,络腮胡,硬挺气质" - - "35岁北美成熟奶爸系男模,浅胡茬,栗色短发,温暖亲切笑容" # 新增 + - "22岁纽约街头滑板男模,深色脏辫,苍白肌肤,酷感眼神" + - "27岁美式机车风男模,粗犷轮廓,短络腮胡,年轻硬朗气质" + - "26岁北美暖男系男模,浅胡茬,栗色短发,温暖亲切笑容" - # ========== 男模 - 欧美/商务/雅痞类(适合重磅纯色T恤、卫衣) ========== - - "25岁英伦雅痞型男,复古油头,短胡茬,熟男气质" - - "30岁华尔街商务精英男模,背头,沉稳内敛气质,干净皮肤" - - "28岁意大利型男,深邃五官,意式凌乱卷发,健康肌肤" - - "32岁法式儒雅大叔,微卷发,金丝眼镜,温润如玉气质" + # ========== 3. 商务/雅痞系(适合重磅纯色T恤/卫衣) ========== + - "25岁英伦雅痞型男,复古油头,短胡茬,痞帅气质" + - "30岁华尔街商务精英男模,背头,干净皮肤,沉稳内敛气质" + - "28岁意大利型男,意式凌乱卷发,深邃五官,健康肌肤" + - "26岁法式文艺男模,微卷棕发,金丝眼镜,温柔书卷气" - # ========== 男模 - 极简/暗黑/高街类(适合纯色基础款、暗黑风) ========== + # ========== 4. 极简高街系(适合纯色基础款/暗黑风) ========== - "25岁欧美高冷男模,银白发色,苍白冷白皮,锋利眼神" - - "29岁柏林暗黑系青年,苍白皮肤,冷感眼神,黑色直发" + - "29岁柏林暗黑系青年,黑色直发,苍白皮肤,冷感眼神" - "26岁东欧赛博风男模,高颧骨,清冷骨相,浅色唇" - "33岁北欧性冷淡风男艺术家,银色短发,疏离眼神,极简气质" - # ========== 男模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备) ========== # 新增分组 + # ========== 5. 多元族裔系(北美/欧洲站覆盖) ========== - "27岁非裔阳光型男模,健康深色肌肤,利落寸头,爽朗笑容" - "31岁非裔商务型男模,修剪整齐的短须,挺拔身姿,可靠稳重气场" - "26岁拉丁裔热情男模,微卷黑发,蜜色肌肤,明亮笑容" - "29岁印度裔斯文男模,黑短发,浓眉,温和儒雅气质" - "30岁中东裔深邃男模,黑色微卷发,轮廓立体,沉稳内敛" - - "33岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容" # 大码:覆盖plus size市场 + - "25岁东南亚裔阳光男模,黑色短发,蜜色肌肤,清爽亲和笑容" + + # ========== 6. 大码系(Plus Size市场) ========== + - "29岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容" + - "27岁欧美大码阳光男模,浅络腮胡,魁梧体型,憨厚亲切笑容" diff --git a/configs/seed_shot_templates.yaml b/configs/seed_shot_templates.yaml index 52561d9..0f3f157 100644 --- a/configs/seed_shot_templates.yaml +++ b/configs/seed_shot_templates.yaml @@ -1,174 +1,176 @@ -# 种草图提示词模板库 —— 跨境电商详情页专用版(稳定出图+零遮挡强约束+得体姿态+全亮调场景版) +# 种草图提示词模板库 —— 跨境电商上衣专用版(服装主体构图+印花中心+零遮挡强约束+得体姿态+全亮调场景版) # 占位符:[商品名称]、[材质]、[模特特征]、[服装风格] -# 摄影参数说明:35mm/50mm镜头控制自然透视,f/1.4-f/2.8控制景深虚化,8k强化面料细节,3:4符合电商标准长图比例。 +# 核心逻辑:画面主体永远是衣服。取景自头顶留白至下摆以下,整件衣服100%完整入画, +# 头部占比≤1/4且五官弱化,视觉中心恒定为胸前印花。 +# 摄影参数说明:50mm为主力镜头控制自然透视,f/1.4-f/2.8控制景深虚化,8k强化面料细节,3:4符合电商标准长图比例。 seed_shot_templates: # ========================================== # 一、 高转化白底/灰底主图区(聚焦正面印花) # ========================================== - - name: "纯白背景正面全展示" + - name: "纯白背景正面服装主体展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,身体正对镜头微侧15度,眼神直视镜头,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部位于画面最上沿且占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔自然,肩部放松下沉,双手自然垂放在身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白无缝背景纸前,身体正对镜头微侧15度,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。 - - name: "高级浅灰背景全展示" + - name: "高级浅灰背景服装主体展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔自然,肩部放松,双手自然垂放或单手插兜姿态收尾于身体两侧,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。 - - name: "纯白背景双手展示下摆" + - name: "纯白背景下摆与版型展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手背在身后或自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景半身构图,模特站在纯白背景纸前,眼神直视镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,重点同时覆盖胸前印花与下摆缝线,二者均须完整清晰;头部占比不超过画面高度的四分之一,五官弱化不做强调,禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,双手背于身后或自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,身体正对镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺及宽松版型。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。 - - name: "低饱和克莱因蓝背景棚拍" + - name: "低饱和克莱因蓝背景服装主体棚拍" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特在低饱和克莱因蓝的无缝背景纸前摆出具有张力的站姿,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔有张力,肩线平直,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在低饱和克莱因蓝的无缝背景纸前,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。 - - name: "纯白背景微动态走动展示" + - name: "纯白背景微转体动态展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,呈现正对镜头的微动态走动状态,整体呈现[服装风格]的调性,展示衣服正面印花在动态下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身向一侧微转约30度后回望镜头方向,肩部放松,双手自然垂放于身体两侧,转动幅度不得使印花侧转到画面边缘之外,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白无缝背景纸前,呈现轻微转体的微动态,展示衣服正面印花在转动下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。 # ========================================== # 二、 面料与印花特写区(打消购买顾虑) # ========================================== - name: "纯白背景领口细节特写" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示衣服的领口、袖口等工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装细节构图:镜头聚焦领口与上胸区域,取景范围为鼻底以下至腰部以上,画面主体是这件[商品名称]的领口罗纹、缝线工艺、肩部做工与上胸区域的印花上缘;模特面部仅保留唇部以下的局部作比例陪衬,禁止以五官为画面焦点,禁止把镜头对准脸部,禁止裁切所要展示的衣服细节部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特肩部放松下沉,双手不在画面内,绝对不可遮挡衣服任何部位。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,画面聚焦展示衣服的领口工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,85mm镜头,f/2.0光圈,8k分辨率。 - name: "明亮室内逆光面料透气质感展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,边缘形成强烈的轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是被逆光照透的面料与胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,在衣服边缘形成柔和轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。 - name: "双手拉扯领口弹性展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捏住领口两侧向外轻拉展示弹力,绝对不可将手放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,50mm镜头,f/2.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装细节构图:镜头聚焦领口与上胸区域,取景范围为鼻底以下至腰部以上,画面主体是双手轻拉领口的拉伸状态、领口罗纹回弹纹理与上胸区域的印花上缘;模特面部仅保留唇部以下的局部作比例陪衬,禁止以五官为画面焦点,禁止把镜头对准脸部,禁止裁切所要展示的衣服细节部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸。【动作硬约束】模特双手捏住领口左右两侧边缘向外轻拉展示弹力,手部仅接触领口边缘线,绝对不可移向胸口中央遮挡印花。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,85mm镜头,f/2.8光圈,8k分辨率。 - name: "低头凝视正面印花特写" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特站在纯白背景纸前,微微低头凝视衣服胸前的印花图案,整体呈现[服装风格]的调性,画面以斜俯视角度聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;模特微微低头使视线落在胸前印花上,头部随低头角度自然变小并略带透视缩短,成为氛围陪衬,画面的绝对视觉中心依然是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特微微低头注视胸前印花,头发不得垂落遮盖肩部与印花区域,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡衣服。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,85mm镜头,f/2.0光圈,8k分辨率。 # ========================================== # 三、 居家生活场景区(营造松弛感与舒适度) # ========================================== - - name: "居家沙发坐姿场景" + - name: "居家沙发服装主体坐姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手自然放在腿上或沙发扶手上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特得体地坐在明亮现代极简风格的客厅布艺沙发上,身体正对镜头微侧,姿态松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的舒适度。室内采用柔和的高调漫反射自然光,背景为高度虚化的明亮居家环境。画面比例为3:4。柯达Portra 400胶片质感,色彩柔和明亮,35mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特端坐于沙发上,上半身自然挺直微侧,双手自然搭在大腿或沙发扶手上远离胸口,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特得体地坐在明亮现代极简风格的客厅布艺沙发上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的舒适度。室内采用柔和的高调漫反射自然光,背景为高度虚化的明亮居家环境。画面比例为3:4。柯达Portra 400胶片质感,色彩柔和明亮,50mm镜头,f/2.0光圈,8k分辨率。 - - name: "居家落地窗自然光展示" + - name: "居家落地窗自然光服装主体展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在高层公寓明亮的落地窗前,身体正前方对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在高层公寓明亮的落地窗前,身体正面对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透,衣服受光充足色彩还原准确。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。 - - name: "周末清晨床头坐姿场景" + - name: "周末清晨床头服装主体坐姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手自然放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特得体地坐在床边,展现周末清晨的松弛感,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的亲肤无拘束感。室内采用清晨明亮的高调自然光,背景为高度虚化的卧室家具。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特端坐于床沿,上半身自然挺直,双手自然搭在大腿上远离胸口,被子或床品绝对不可堆叠遮挡衣服下摆。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特得体地坐在明亮的床边,展现周末清晨的松弛感,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的亲肤无拘束感。室内采用清晨明亮的高调自然光,背景为高度虚化的卧室环境。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。 - - name: "居家厨房场景" + - name: "居家厨房服装主体场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手操作厨具,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特站在现代开放式厨房的中岛台前,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止围裙等其他服饰遮挡衣服。【动作硬约束】模特站在中岛台侧面,上半身放松自然,双手轻搭台面边缘远离胸口,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮的现代开放式厨房中岛台旁,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。 - - name: "周末居家阅读场景" + - name: "周末居家阅读服装主体场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手必须捧书放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特盘腿坐在卧室的羊毛地毯上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料的柔软亲肤感。室内采用柔和的窗光侧逆光,背景为高度虚化的卧室床铺与暖色氛围灯。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止书本遮挡衣服。【动作硬约束】模特坐在明亮飘窗边,书本置于腰腹高度的画面下沿位置,上胸与胸前印花完全不被遮挡,另一只手自然垂放,绝对不可将书举至胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮的窗边阅读,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料的柔软亲肤感。室内采用柔和的窗光侧顺光保证衣服受光均匀,背景为高度虚化的温馨居家环境。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。 # ========================================== # 四、 户外休闲/运动场景区(展现百搭与透气) # ========================================== - - name: "阳光草坪户外站姿" + - name: "阳光草坪服装主体站姿" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的城市林荫道上,身体正对镜头微侧,微微低头微笑,整体展现[服装风格]的氛围,凸显[材质]在自然行动中的透气与百搭。午后阳光透过树叶洒下明亮的光斑,背景为高度虚化的过往行人与都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,35mm镜头,f/1.7大光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止强烈树影大面积压暗衣服。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光充足的城市林荫道上,身体正对镜头微侧,表情放松带浅笑,整体展现[服装风格]的氛围,凸显[材质]在自然光照下的透气与百搭。午后阳光透过树叶洒下明亮光斑,光斑只落在背景上不打在衣服表面,衣服受光均匀色彩准确。背景为高度虚化的都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,50mm镜头,f/1.7大光圈,8k分辨率。 - - name: "都市街头OOTD站姿展示" + - name: "都市街头OOTD服装主体展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手拿咖啡杯放在腰侧,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的现代商业街区外墙前,身体正对镜头微靠在墙上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身微靠墙面,肩部放松,咖啡杯持有手收在腰侧画面最下沿,另一只手自然垂放,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光明媚的现代商业街区外墙前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,光线均匀打亮衣服正面,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。 - - name: "天台蓝天白云站姿场景" + - name: "天台蓝天白云服装主体场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光,无死角的展现服装颜色。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/2.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光均匀打在衣服上,无死角的还原服装真实颜色。画面比例为3:4。徕卡Q2摄影质感,50mm镜头,f/2.8光圈,8k分辨率。 - - name: "自然公园站姿外景" + - name: "自然公园服装主体站姿外景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的公园草坪前,微风吹拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光,画面明亮通透,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止衣物被风吹乱遮盖印花。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光充足的公园绿植前,微风轻拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光板补光,衣服受光均匀色彩鲜亮,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。 - - name: "街头滑板站姿场景" + - name: "滑板公园服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚踩在滑板上,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的滑板公园边缘,身体正对镜头,微风吹拂发丝,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮的阳光形成侧逆光,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/1.7光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在阳光明媚的滑板公园旁,身体正对镜头,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮阳光形成轻微侧光并配合反光板给衣服暗部补光,保证印花区域无死黑,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,50mm镜头,f/1.7光圈,8k分辨率。 - - name: "林荫道骑行停靠场景" + - name: "林荫道骑行服装主体场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚点地双手握把,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特骑着一辆复古自行车停在两旁长满梧桐树的林荫小道上,转头看向镜头微笑,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性。明亮阳光透过树叶洒下光斑,背景为高度虚化的树干与明亮光斑。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特站在复古自行车旁,握车把的手位于画面最下沿腰腹高度,另一只手自然垂放,头微转向镜头方向,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特停在两旁长满梧桐树的林荫小道旁,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性。明亮阳光透过树叶洒下光斑,光斑只点缀背景避开衣服表面,衣服受光均匀,背景为高度虚化的树干与明亮光斑。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,50mm镜头,f/2.0光圈,8k分辨率。 - - name: "海边沙滩站姿场景" + - name: "海边沙滩服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在细软的沙滩上,海风吹起衣摆,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,画面明亮高调,背景为高度虚化的蔚蓝海水、白沙滩与远处海平线。画面比例为3:4。索尼A7R4画质,35mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在细软的沙滩上,海风轻扬发丝,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,衣服过曝得到良好控制,色彩鲜明通透,背景为高度虚化的蔚蓝海水与远处海平线。画面比例为3:4。索尼A7R4画质,50mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。 - - name: "露营帐篷坐姿场景" + - name: "露营帐篷服装主体坐姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手端着搪瓷杯放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的自然光,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,35mm镜头,f/1.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特端坐于露营椅上,上半身自然挺直,搪瓷杯收在腰腹高度的画面下沿,另一只手自然垂放,绝对不可举杯遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的漫射自然光,衣服受光均匀色彩还原准确,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,50mm镜头,f/1.8光圈,8k分辨率。 - - name: "浅水沙滩站姿场景" + - name: "海边浅滩服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在浅水沙滩边,海风吹起衣摆,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与飘逸感。光线为正午到下午的明亮阳光,海面反光形成自然补光,背景为高度虚化的蔚蓝大海、白沙滩与天空。画面比例为3:4。夏日清透色彩,高饱和度,35mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止海水打湿变色衣服。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在海边浅滩旁,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈感。光线为下午的明亮阳光,海面反光形成天然柔和补光打亮衣服正面,背景为高度虚化的蔚蓝大海与天空。画面比例为3:4。夏日清透色彩,高饱和度,50mm镜头,f/2.0光圈,8k分辨率。 - - name: "热带绿植温室站姿场景" + - name: "热带绿植温室服装主体场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在长满大型龟背竹与天堂鸟的植物园温室中,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与透气感。光线为穿透玻璃顶的明亮自然光,形成明亮的光斑,背景为高度虚化的茂密绿植。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.4大光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止植物枝叶伸入前景遮挡衣服。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,与身后绿植保持距离,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在长满大型龟背竹与天堂鸟的植物园温室中,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与透气感。光线为穿透玻璃顶的明亮均匀自然光,衣服表面光影干净无杂乱叶影,背景为高度虚化的茂密绿植。画面比例为3:4。富士Provia 400X色彩预设,50mm镜头,f/1.4大光圈,8k分辨率。 - - name: "现代美术馆纯白站姿外景" + - name: "现代美术馆纯白服装主体外景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代美术馆纯白外墙前,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,55mm镜头,f/2.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特背靠在明亮的现代美术馆纯白外墙前,气质清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,墙面反光为衣服提供均匀补光,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。 - - name: "山顶站姿场景" + - name: "山顶服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在山顶的岩石上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光,画面通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,35mm镜头,f/2.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在山顶的岩石平台上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光并配合天空的自然柔光罩效果,衣服受光均匀色彩通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,50mm镜头,f/2.8光圈,8k分辨率。 # ========================================== # 五、 工作/通勤场景区(展现日常实用度) # ========================================== - - name: "办公场景坐姿展示" + - name: "办公场景服装主体坐姿展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手放在桌上或键盘上不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮的极简办公桌前,身体正对镜头微侧,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,背景为高度虚化的电脑屏幕与办公桌绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止桌面物品升高遮挡衣服下摆。【动作硬约束】模特端坐于办公桌前,上半身自然挺直微侧,双手平放键盘或桌面上远离胸口,屏幕与桌面位于画面最下沿之外,绝对不可抬手遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮的极简办公桌前,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,光线均匀铺满衣服正面,背景为高度虚化的办公环境与绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。 - - name: "咖啡馆坐姿场景" + - name: "咖啡馆服装主体坐姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,单手端着陶瓷咖啡杯放在嘴边不遮挡印花,另一只手自然放在桌上,绝对不可双手交叉抱胸。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮风格咖啡馆的窗边座位上,身体正对镜头微侧,姿态慵懒松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的柔软垂坠感。明亮的自然光从大窗户斜射进来,画面通透,背景为高度虚化的木质室内装潢、绿植与朦胧的咖啡蒸汽。画面比例为3:4。日系胶片质感,富士Provia 400X色彩预设,35mm镜头,f/1.8大光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止咖啡杯与杯蒸汽遮挡衣服。【动作硬约束】模特端坐于窗边座位,上半身自然挺直微侧,咖啡杯托在手心置于腰腹高度的画面下沿,杯体远离胸口区域,另一只手自然放在桌面,绝对不可将杯子抬至胸前。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在明亮风格咖啡馆的窗边座位上,姿态松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的柔软垂坠感。明亮自然光从窗户侧面射入并配合室内暖光补足衣服正面亮度,画面通透,背景为高度虚化的木质装潢与绿植。画面比例为3:4。日系胶片质感,富士Provia 400X色彩预设,50mm镜头,f/1.8大光圈,8k分辨率。 - - name: "书店站姿翻阅场景" + - name: "书店服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捧书放在腰间不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在明亮的高大木质书架前翻阅一本精装书,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止书本遮挡衣服。【动作硬约束】模特上半身挺拔微侧,持书的手将书收在腰腹高度的画面下沿视线瞥向书页,另一只手自然垂放,绝对不可将书举起靠近胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮的高大木质书架前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,射灯角度经过调整避免在衣服上留下脸部阴影,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。 - - name: "复古家具店站姿场景" + - name: "复古家具店服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在摆满上世纪老物件的中古家具店内,眼神安静,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的质感。店内采用明亮的自然光漫反射,背景为高度虚化的胡桃木柜子与明亮光斑。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止家具道具伸入前景遮挡衣服。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,与周围家具保持安全距离,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在摆满上世纪老物件的中古家具店内,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的质感。店内采用明亮的自然光漫反射,环境家具均为背景虚化元素,不在衣服上投下明显阴影,背景为高度虚化的胡桃木柜与明亮光斑。画面比例为3:4。富士Provia 400X色彩预设,50mm镜头,f/1.8光圈,8k分辨率。 - - name: "艺术画廊站姿场景" + - name: "艺术画廊服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在灯光柔和明亮的现代艺术画廊内,侧身凝视一幅巨大的抽象画后转过头来正对镜头,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业的顶光灯打出柔和的漫反射,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,35mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特由侧身观展状态转回正对镜头,上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在灯光柔和明亮的现代艺术画廊内,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业顶光灯打出均匀漫反射铺满衣服正面,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,50mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。 - - name: "超市货架站姿场景" + - name: "超市货架服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手推车不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特推着购物车或站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,姿态俏皮个性,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源打亮环境,背景为高度虚化的彩色商品与灯轨。画面比例为3:4。赛博潮流摄影,35mm镜头,f/1.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止彩色货架光线污染衣服色调。【动作硬约束】模特上半身挺拔,扶购物车的手位于画面最下沿,另一只手自然垂放,姿态俏皮个性,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源仅照亮背景,模特所在的前景由柔和补光灯独立打亮,确保衣服色彩不受环境色影响而偏色,背景为高度虚化的彩色商品。画面比例为3:4。赛博潮流摄影,50mm镜头,f/1.8光圈,8k分辨率。 # ========================================== # 六、 情绪氛围与高亮特色场景区(提升品牌调性) # ========================================== - - name: "明亮工业风咖啡馆站姿展示" + - name: "明亮工业风咖啡馆服装主体展示" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在明亮工业风咖啡馆的纯白水泥墙前,眼神清冷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,背景为高度虚化的明亮通道与绿植。画面比例为3:4。赛博潮流电商摄影,55mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在明亮工业风咖啡馆的纯白水泥墙前,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,衣服正面受光均匀立体,背景为高度虚化的明亮通道与绿植。画面比例为3:4。潮流电商摄影,85mm镜头,f/2.0光圈,8k分辨率。 - - name: "录音棚站姿场景" + - name: "录音棚服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手调音不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特戴着监听耳机站在专业麦克风前调音,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用录音棚明亮的暖色聚光灯与整体白光对比,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止耳机线垂落遮挡印花。【动作硬约束】模特佩戴监听耳机站立于麦克风架旁,麦杆收至脸侧避开胸口区域,一只手轻轻扶住耳机耳罩位于画面侧缘,另一只手自然垂放,绝对不可让手部或设备遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特在明亮的录音棚内,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用明亮的暖色聚光灯配合正面柔光板补光,确保印花区域通透明亮,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。 - - name: "暖光清吧坐姿场景" + - name: "暖光清吧服装主体坐姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手把玩酒杯放在吧台上不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在氛围感清吧的吧台前,眼神微醺迷离,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄色点光源与背景微弱的白光对比,背景为高度虚化的酒瓶阵列与杯光交错。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止酒杯遮挡衣服。【动作硬约束】模特端坐于吧台前,上半身自然挺直微侧,酒杯托在手心置于吧台面的画面下沿高度,远离胸口,另一只手自然垂放,绝对不可举杯遮挡印花。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特坐在氛围感清吧的吧台前,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄主光源配合正面白色补光板,衣服正面曝光充足色彩不偏橙,背景为高度虚化的酒瓶阵列与明亮灯带。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。 - - name: "日落黄昏街头站姿场景" + - name: "日落黄昏街头服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手插兜不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特倚靠在城市明亮的天桥栏杆旁,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏下的视觉效果与[材质]的质感与垂感。采用日落前明亮的黄金时刻光线与边缘光,背景为高度虚化的明亮车流与城市剪影。画面比例为3:4。赛博朋克潮流摄影,35mm镜头,f/1.4大光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止剪影化处理导致衣服发黑。【动作硬约束】模特上半身微倚天桥栏杆,肩部放松,双手自然垂放于身体两侧胯部附近,栏杆金属管避开衣服下摆高度,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特倚靠在城市明亮的天桥栏杆旁,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏暖光下的视觉效果。采用日落黄金时刻的明亮顺侧光,金色阳光直接铺满衣服正面而非逆光,色彩温暖且印花清晰锐利,背景为高度虚化的明亮车流与城市天际线。画面比例为3:4。潮流摄影,50mm镜头,f/1.4大光圈,8k分辨率。 - - name: "旱冰场站姿场景" + - name: "旱冰场服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特穿着旱冰鞋站在复古旱冰场中央,姿态从容,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。明亮的紫粉色调与射灯打在模特身上,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,35mm镜头,f/1.4大光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止场地彩光污染衣服原有色彩。【动作硬约束】模特上半身挺拔微侧,肩部放松,双手自然垂放于身体两侧胯部附近,姿态从容,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特站在复古旱冰场中央,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。场地采用明亮的主射灯独立照亮模特并配有正面柔光,衣服保持自身固有色不被环境粉紫光染色,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,50mm镜头,f/1.4大光圈,8k分辨率。 - - name: "明亮的现代地下车库站姿场景" + - name: "明亮现代地下车库服装主体站姿场景" prompt: | - 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,眼神冷酷,身体转回正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯照明,画面清透无暗角,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。赛博朋克潮流摄影,55mm镜头,f/2.0光圈,8k分辨率。 + 【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【构图约束】服装主体构图:取景范围自模特头顶上方少许留白起,至衣服下摆下方一掌宽处止,这件[商品名称]的领口、肩线、双袖、下摆与正面印花必须100%完整入画且无遮挡;头部占比不超过画面高度的四分之一,五官弱化不做强调,画面的绝对视觉中心是胸前印花;禁止面部特写构图,禁止裁切衣服任何部位。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特上半身挺拔背靠水泥柱,肩部放松,双手自然垂放于身体两侧胯部附近,绝对不可抬手遮挡胸口。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯正面补光组合,画面清透无暗角,衣服受光均匀对比适中,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。潮流摄影,85mm镜头,f/2.0光圈,8k分辨率。 diff --git a/db/spu_sku.db b/db/spu_sku.db index 9fe53cac2e713e7fec3470c613c251a0f40f9e0a..ad851cbf8045d2b59308a28cc46b798f0d4c052a 100644 GIT binary patch delta 3624 zcmai1eQ*=k5x>*j>GUL>WTQJIS!W4@O$-i1GGOA8)*pNsW5))Y7(xgRU@$gdIw9dB z8H$>s7d!=w59UqCq=bec1Y8@iv5alNG}F?rP(lDZlO~8xz~zrjn#{mVrzPpWlRsn} zwVHW<+}qu^`}Y0zx2s)|4Z9*68a(X#2h`c@`)4YTmP}$8CXt=M+9%t}r5ful%SrPM z^c_k!Ef!xl)(dUy1pc?&9}M3bRFcxb{>vPE1shbxnVsg=ymc;jz@Pb#;NknbTlzbX z^>?+U-)leC-`W0k_a5?1ujJ7FQr^rO8>_vo%A$If+VZisnf~~;7g1tx6XrnONA?vW zmu=4cK)`?Ry*>BB;r`Cf#`V9`wk^470v%>ZN83DBKqFKZRovu*i$gmh1a0BcwpPQk zCRRK1{4KhK6~9fY6JO%=q;&zjxooK99RArJN+v#^t zci+GG79?G(sOl6qVcSqfAZs)HnXxGWu{2T~h!Gzr|7k}aty1O1wb(Ln`4p`|mFcBV z>Vi$!41#R(jYDy}=V$t}elTj1YvSG}J33JkHA1goM2&RRBspjNpoxx|ORZm2HnZCH z)wi_8YkDbU5{C?3?ks;GzG)OP2{h_K$jtChkGY-FTD)#ItF_kjYB`&3iB(vnx|&*- z@6%=>~>qaG}F4$g3X7}1=C&eJJD&(5!NwR`Co8H*^do>geN?f z75o{lP#I5#BfvNujN9RMFL%42a=44r+z#ioM0;6YL9yRI?QUpq|G^_r8d2<$+h%Sx zsb#q$@~pu5tXuH&a7YvfgiV(njtonn`hlrU1s1C~2)T8Wpz84kyx z2V5$tFGfZ5{90utUaD_1jKU1XM^153{~N<~0@uuPt8qCQSAv}6pDZUNEL^MyJ(M?+ zXW}|Qr)-qYFtEa)Q*8ijm4Z7TxqJpC*~%CC{Z9{_5{65^Y!El%VtQBd(ZL_2*>&upR%%@Nv5=^Q%+gK}Td>89srgDEa+y)LbZv)JJvm zqXKUSI!izE=%_F#C!BsdHd^vPDUOgX$Bk>lXBb4)##lq4+a5esMlrkEwNk;N! zH@YHL;8`?6?c+qf`mMt_A*`yL&?6G7@sD(+0i+AUU`JL6VEs3t9u(>^hk7ib9&3A# zWK2k%!`LO8Pnu;du&gkL(A%aH;yGiNaGAfteQtPQ@G^7QnFsQcOu;(*#9)=U)WWi| z_yuzGn$!`_{6xCQqBS@TjC|3i>$H{41*@UNl5N+ePU~8nN>vylW9$59Jf zbT0DOK52|OW`?`dCv|{^Z&<^AQxeF9PbF!T+B)n7HJu+&VHt4Gv}k!9BPE|n55;v@ z9Vs5FzBm3RHG>n+u#?iyCBYf5(L+p!iTAqXwN~OJuz_DR>rPw1ADx8mA3P3Y5qZ=r z9i$zW&NG4;yQzO@(TaAbS|;Fwx9B6?KvQ1&h13IKxJHD*lUcVV+hcbPhayraVhcs= zp-4g~k{F7NX^%LJxf3f`d%EozX^nM<<=5tqxQFPD!NWdZ^^Qw+rpbr3%?^)LS`+}Ds-oymbLcZ(LTM3nHg;-&{?kOQis zqH4Tmly>Z`bnR)=quSP5yaBYc9W-Nf(oj6*KyUF-Rg@yPf`e5t4#-!NWQ(9r;CdkX zxJY&;D@JxBWe4X#Pfw8rQa4UML%vUuZ}7oIxF$yWaJ+nuG*6IWG+%_PV+4=~BWX&N zTc}{|b2JJ_A#K0}SMUHB7I&wtA&aryAHFPegz@#}D+J|JhHp=yc_#O<=;g`94s zE(e~NTpX(y&9qtukVxN*yx>xNz|Grf7S!+u2}&LZ#Q54ksBScOa7fcT7~H&pJZeWt u+kMqB1>xl~h delta 3132 zcmaJ@Z%|a%6@PEv+kJa?_w54vmfd9+RvI?MKhd|SsMQ8RG>Rfb(1;-5A5;_q?Zi=I zYTb0$I~cV%&v;v>B1D>6Cn5;h1r%v(nwrdHDk`F)b~_CaCU&G_Kjeefp7-7ok;HyD zv$N;^&bjBE^E>CB{o#uJ;fgvJed36F5q+Yyv@vJ0IgN^@9@#8Y%=!EqrcK5JhE{Y7 zY4nr1TwShq7kijFtoe}soO(nO>*&u}|0=8_jmwd{ZNaL8w2`7H|2E9JDZ58%-5%HW zX-vCD`eb!qTXrlF_2uxZ4>!t!-#Wjhdky*ev^zkGPbfQ?*0ECepY9{e|0s`N%<{M%8^p zR>as`QmLDjDk^Vso3K#^D2ajlQ+#5?{6Pz(F} z#6_(ZMV}n&soF(VZ@*pj7HMUfPP&l%2{CRmqG39@&3GN)olpcTc|bEJT_kL?hPgzq zRe?<`Q*g}06dCiAJHet>4y=h(+ifngFNk!Z14gEqW;Wq$vTvGTCzF;T zEtz3qv}WJzuY)zi7Xp#*kxO|j9XW{C%zVSF$ID6DO5~QI+*sPiAoz)e0bxP3AUY5Z zL?3!$L993#nkt$aby(a`)e=*QF<=Ow7X2OWUtF{3>qAa#cTL{Z)7# zv1FjDWYo;Kjaj)qe-WMw%>zAg9yv_;85we+pAinCbaMrGj+?To5R|thW8!%cPJQMg zcX2+r7_2!BD2216Arn0!b&Jsem%Y~K&&M-WGQ7V8b&!Rxqq{~~U5wnn;bkto)=1T5qZ1}oiob4{Vtvk+?U(cp$RjTH>%Vy!t zv-y>#t;YQZf|~W$xo^36U50imdyr|>bkLnt7fhx)J#af~jN1wJ|HJCb++MtF4C#q| zG5s$C8<&o;(Io&GQzw!!1Y|r;b8=1d@e(!1dqR-o=^c=IRh_A-CwGGSekZ8qiJy-% zRQ2sH)WLv;9JzpQDdZ@_3*k~_V0oCqLx)M-Mbx8**^oA-jwAP55R%6o%&>0Y{%hm7 zf29>!6%Ht5&*?_r0ARs0fHf<3*=jLG{Nd(+xOMD&xJWwU5|n1*6})fOU`JrE z{v3Bn_oenO`%p8Vbx=DQ9~5!9-d}=~$7pROSNnO+0K^*K9K_iRBfA8D$RdEBgtxJLQK1YF|t zxS11iJ-RYH2{6PzO>mOFem+L7aR+EU-)XPhifG|gN&s!5{N_2y#oN4|GpG)w=AwF% zHo$ubzQgwe&ixFxZ{Qh@930@g^;>Zq949-a-aI=gyvLjP$Opm;4jGe1w;z>Ki#^8O z*a^7#uwB@shK+)}59sVKO7qJ5{2w7~2Uw^dBX1AAHT*_Zai!VqAy|a{)aQe;4NkYOta=D{a@NceGu((bI@e|H*h8C zogyR<)+@Agg}6fLA(vmn0^D+5*lAt49Uf~*-yTS){+f}>fmxVS`oGjdm zsJF>#4=H#>I3Lk$RWx+AG-=&2GWv?(is-kf$mx@X^HH+Pjkrv{+-s$*R`M`Ouo>a` zg>59*B*##aDAdc=Zqr7*`TIC=-b5Tzg*V9!=q5Rdf;L8+_!QTY=qW-sD#II9Z^{Cu zVD;kI2BlmoZ%gXeU!5XcBPXW{1I9(LE0p6Bu$=nciyylvJsbxVR(KZicWv`MId*)(gyBU8sEGSA=PLk{xTO4^P*mR^b)^-z&cB`puw%fR str: - """图池路由:简报达标 → done;图池还有未消费图片 → analyze(继续分析,不搜索); - 图池不足 → search(新一轮搜索);轮次耗尽 → done。""" + """图池路由:按简报池实时需求补——简报池还有待处理/在途简报 → 不分析不采集; + 简报池空闲 + 图池有未消费图片 → 补分析;简报池空闲 + 图池空 + 简报不足 → 搜索采集; + 简报达标 → done。不按任务数量提前结束,先消耗图池存量,不够用了才主动采集。 + 致命 503(图像服务不可用)→ 立即终止,不再搜索/分析,直接收尾合成模板。""" + # 致命 503:图像服务不可用,重试无效 → 提前终止,未完成产品废弃,直接收尾 + pipe = state.get("pinterest_pipeline") + if pipe is not None and hasattr(pipe, "is_fatal_503_aborted") and pipe.is_fatal_503_aborted(): + print("[pinterest_route] ⛔ 图像服务 503 已终止任务,停止搜索/分析,直接收尾合成模板") + return "done" + target = int(state.get("pinterest_target") or 0) if target <= 0: target = 1 @@ -72,11 +81,14 @@ def _pinterest_route(state: Dict[str, Any]) -> str: if max_rounds <= 0: max_rounds = max(target * 2, 5) - if len(briefs) >= target: - print(f"[pinterest_route] 简报已达目标 {len(briefs)}/{target},结束") - return "done" + # 简报池还有待处理/在途简报 → 先让后台消化,不分析新图也不采集 + if pipe is not None and hasattr(pipe, "pending_count"): + pending = pipe.pending_count() + if pending > 0: + # 日志由 pinterest_wait 节点进入时统一打印(阻塞等待消化,避免此处高频刷屏) + return "wait" - # 图池还有未消费图片 → 继续分析(不搜索) + # 简报池空闲 → 检查图池:还有未消费图片 → 补分析(不管简报离目标差多少,先把这批用完) try: from graph.pinterest import load_image_pool, load_used_images, pool_unused_images pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "") @@ -85,24 +97,31 @@ def _pinterest_route(state: Dict[str, Any]) -> str: except Exception: # noqa: BLE001 unused = [] if unused: - print(f"[pinterest_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target})") + print(f"[pinterest_route] 简报池空闲,图池还有 {len(unused)} 张未消费图片,补分析" + f"(简报 {len(briefs)}/{target})") return "analyze" - # 图池不足 → 搜索 + # 简报池空闲 + 图池空 → 检查简报是否已达标 + if len(briefs) >= target: + print(f"[pinterest_route] 图池已空,简报已达目标 {len(briefs)}/{target},结束") + return "done" + + # 简报池空闲 + 图池空 + 简报不达标 → 新一轮搜索采集 if rounds >= max_rounds: - print(f"[pinterest_route] 已达最大轮次 {max_rounds},简报 {len(briefs)}/{target},按现有结果继续") + print(f"[pinterest_route] 已达最大轮次 {max_rounds},图池已空,简报 {len(briefs)}/{target},按现有结果继续") return "done" if not terms and rounds > 0: - print(f"[pinterest_route] 无可用搜索词,停止搜索(简报 {len(briefs)}/{target})") + print(f"[pinterest_route] 无可用搜索词,图池已空,停止搜索(简报 {len(briefs)}/{target})") return "done" - print(f"[pinterest_route] 图池不足,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target})") + print(f"[pinterest_route] 简报池空闲,图池不足,简报未达标,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target})") return "search" def build_pinterest_graph(): """Pinterest 参考模式图(按需搜索循环 + 简报池并发生成): pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze - → [pinterest_route] 简报不足 → 回到 pinterest_search;达标 → pinterest_finalize + → [pinterest_route] 简报池还有在途 → wait(等待后台消化)→ 回到路由; + 简报池空闲 + 图池有图 → analyze;图池空 + 简报不足 → search;达标 → pinterest_finalize (排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export """ from graph.nodes import ( @@ -113,11 +132,27 @@ def build_pinterest_graph(): from graph.nodes.pinterest_finalize_node import pinterest_finalize_node from graph.nodes.pinterest_init_node import pinterest_init_node + @with_fallback("pinterest_wait") + def _pinterest_wait(state: Dict[str, Any]) -> Dict[str, Any]: + """简报池还有在途简报时阻塞等待后台消化(最多 60s),避免轮询刷屏。 + + 用 pipeline.wait_idle 的 Condition 阻塞等待(_process_one 完成即唤醒), + 消化完才返回,路由重新判断;超时兜底返回,避免死等。 + """ + pipe = state.get("pinterest_pipeline") + if pipe is not None and hasattr(pipe, "wait_idle"): + pipe.wait_idle(60) + else: + import time as _t + _t.sleep(5) + return {} + builder = StateGraph(AgentState) builder.add_node("pinterest_init", pinterest_init_node) builder.add_node("pinterest_search", pinterest_search_node) builder.add_node("pinterest_scrape", pinterest_scrape_node) builder.add_node("pinterest_analyze", pinterest_analyze_node) + builder.add_node("pinterest_wait", _pinterest_wait) builder.add_node("pinterest_finalize", pinterest_finalize_node) builder.add_node("template_export", template_export_node) @@ -126,10 +161,12 @@ def build_pinterest_graph(): builder.add_edge("pinterest_search", "pinterest_scrape") builder.add_edge("pinterest_scrape", "pinterest_analyze") builder.add_conditional_edges("pinterest_analyze", _pinterest_route, { - "analyze": "pinterest_analyze", # 图池还有未消费图片 → 继续分析(不搜索) - "search": "pinterest_search", # 图池不足 → 新一轮搜索 + "analyze": "pinterest_analyze", # 简报池空闲 + 图池有未消费图片 → 补分析(不搜索) + "search": "pinterest_search", # 简报池空闲 + 图池空 + 简报不足 → 新一轮搜索 + "wait": "pinterest_wait", # 简报池还有在途 → 等待后台消化 "done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池) }) + builder.add_edge("pinterest_wait", "pinterest_analyze") builder.add_edge("pinterest_finalize", "template_export") builder.add_edge("template_export", END) return builder.compile() @@ -232,5 +269,9 @@ def run_pinterest_ref( or int((global_config.get("product") or {}).get("spu_count") or 0) or 1, "pinterest_rounds": 0, "pinterest_attempted": [], + "pinterest_images": {}, + "pinterest_briefs": [], + "pinterest_login": {}, + "pinterest_pipeline": None, } return compiled.invoke(state) diff --git a/graph/backends/openai_image_backend.py b/graph/backends/openai_image_backend.py index 20a25c9..18b3e6a 100644 --- a/graph/backends/openai_image_backend.py +++ b/graph/backends/openai_image_backend.py @@ -10,6 +10,7 @@ 所有请求跳过环境代理(NO_PROXY),适配用户挂 VPN 时直连国内/自建网关。 """ import base64 +import json import time from pathlib import Path from typing import Optional @@ -31,6 +32,17 @@ _FINAL_STATUS = ("succeeded", "completed", "done") _FAIL_STATUS = ("failed", "error") +def _retry_after(resp, attempt: int) -> int: + """429 限流等待秒数:优先取网关 Retry-After 头,否则指数退避(3/6/9s)。""" + ra = resp.headers.get("Retry-After") + try: + if ra is not None and ra.isdigit(): + return min(int(ra), 30) + except Exception: # noqa: BLE001 + pass + return 3 * (attempt + 1) + + def _shrink_blob_to_2mb(img_path: str, blob: bytes, max_bytes: int = 2 * 1024 * 1024) -> bytes: """把大图压缩到 = 500: + # 5xx 服务端错误(500/502/503/504 超时等):网关临时故障/过载,退避后重试 + body = resp.text or "" + last_err = f"图像 API {resp.status_code}: {body[:300]}" + wait = 3 * (attempt + 1) + print(f"[img] 图像 API {resp.status_code}(网关临时故障/超时),等待 {wait}s 重试 {attempt + 1}/3") + time.sleep(wait) + continue if resp.status_code >= 400: body = resp.text or "" if "content_policy" in body and attempt < 2: @@ -193,18 +223,24 @@ class OpenAIImageBackend(ImageBackend): raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}") try: return _resolve_task_or_sync(resp.json(), base_url, headers, out_path) + except json.JSONDecodeError as e: + # 空/非 JSON 响应:多为网关过载返回空 body → 退避后重试, + # 避免即时重压触发网关 429(用户实测空响应风暴 → 429) + last_err = str(e) + wait = 3 * (attempt + 1) + print(f"[img] 网关空响应(JSON 解析失败),等待 {wait}s 重试提交 {attempt + 1}/3: {e}") + time.sleep(wait) except Exception as e: # noqa: BLE001 last_err = str(e) - print(f"[img] 第 {attempt + 1} 次提交异步失败,重试同步提交: {e}") + print(f"[img] 第 {attempt + 1} 次提交异步失败,等待后重试同步提交: {e}") + time.sleep(2 * (attempt + 1)) raise RuntimeError(f"图像合成多次提交均失败: {last_err}") def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "", seed: Optional[int] = None) -> str: """纯文生图:生成白底纯印花设计稿(standalone pure print design)。 size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。 - seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。 - background: 配置 compose.background="transparent" 时传 background 参数 → 透明背景 PNG - (gpt-image-1/2 等模型支持;网关不支持该参数时会被忽略或由网关兜底)。""" + seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。""" cfg = self._cfg api_key = cfg.get("api_key", "") model = cfg.get("model", "gpt-image-1") @@ -220,17 +256,32 @@ class OpenAIImageBackend(ImageBackend): "size": size or cfg.get("size", "1024x1024"), "model": model, "response_format": "b64_json", - "execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定) } + # execution_mode 仅对明确支持的网关(如 ai-media)传;yunfei 等标准 OpenAI 网关 + # 不认识该参数,硬传会导致空响应,故默认不传,仅 config 显式配置时才带上 + em = str(cfg.get("execution_mode") or "").strip() + if em: + data["execution_mode"] = em if seed is not None: data["seed"] = seed - bg = str(cfg.get("background") or "").strip() - if bg: - data["background"] = bg # 如 "transparent"(透明背景 PNG) last_err: Optional[str] = None for attempt in range(3): resp = requests.post(f"{base_url}/images/generations", headers=headers, json=data, timeout=300, proxies=NO_PROXY) + if resp.status_code == 429: + # 限流:尊重网关负载退避等待再重试,避免硬撞雪崩 + wait = _retry_after(resp, attempt) + print(f"[img] 图像 API 429 限流,等待 {wait}s 重试 {attempt + 1}/3") + time.sleep(wait) + continue + if resp.status_code >= 500: + # 5xx 服务端错误(500/502/503/504 超时等):网关临时故障/过载,退避后重试 + body = resp.text or "" + last_err = f"图像 API {resp.status_code}: {body[:300]}" + wait = 3 * (attempt + 1) + print(f"[img] 图像 API {resp.status_code}(网关临时故障/超时),等待 {wait}s 重试 {attempt + 1}/3") + time.sleep(wait) + continue if resp.status_code >= 400: body = resp.text or "" if "content_policy" in body and attempt < 2: @@ -243,7 +294,15 @@ class OpenAIImageBackend(ImageBackend): raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}") try: return _resolve_task_or_sync(resp.json(), base_url, headers, out_path) + except json.JSONDecodeError as e: + # 空/非 JSON 响应:多为网关过载返回空 body → 退避后重试, + # 避免即时重压触发网关 429(用户实测空响应风暴 → 429) + last_err = str(e) + wait = 3 * (attempt + 1) + print(f"[img] 网关空响应(JSON 解析失败),等待 {wait}s 重试生成 {attempt + 1}/3: {e}") + time.sleep(wait) except Exception as e: # noqa: BLE001 last_err = str(e) - print(f"[img] 第 {attempt + 1} 次生成异步失败,重试同步提交: {e}") + print(f"[img] 第 {attempt + 1} 次生成异步失败,等待后重试同步提交: {e}") + time.sleep(2 * (attempt + 1)) raise RuntimeError(f"图像生成多次提交均失败: {last_err}") diff --git a/graph/llms/mock_backend.py b/graph/llms/mock_backend.py index 2f17df6..cc24945 100644 --- a/graph/llms/mock_backend.py +++ b/graph/llms/mock_backend.py @@ -180,14 +180,11 @@ class MockBackend: paths = list(image_paths or []) return [{ "topic": term, - "concept": f"(启发式兜底)围绕「{term}」做原创{art_style}风格印花", - "motif": motif, - "art_style": art_style, - "color_palette": palette, - "composition": composition, + "suitable_for_print": True, "negative_prompt": negative, "image_prompt": (f"{motif}, {art_style}, {palette}, {composition}, " - f"original {art_style} t-shirt print design"), + f"original {art_style} t-shirt print design, " + f"no brand logo, no trademark, no character, no watermark"), # 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应) "ref_images": [str(paths[i])] if i < len(paths) else [], "source": "pinterest", diff --git a/graph/llms/openai_compat_backend.py b/graph/llms/openai_compat_backend.py index 913643a..f9b8f5f 100644 --- a/graph/llms/openai_compat_backend.py +++ b/graph/llms/openai_compat_backend.py @@ -318,31 +318,56 @@ def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str: # —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)—— # image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接), # 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。 -PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD (print-on-demand) T-shirt design analyst. -You receive Pinterest reference images for one search term. For each image, extract the VISUAL CONCEPT -(style, mood, motif, color palette, composition) that makes it appealing, then produce an ORIGINAL -T-shirt print design brief that captures that VIBE WITHOUT copying the image. +PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD T-shirt design analyst. Given ONE Pinterest reference image, +judge whether it can inspire a T-shirt print, then write an ORIGINAL design brief +capturing its vibe WITHOUT copying. +Your image_prompt will be sent TOGETHER WITH this reference image to an image +generator, so it must actively override visual imitation. RULES: -- NEVER copy the image, never reproduce the exact artwork, characters, logos, or any text from it. -- Extract only the abstract style/mood/motif concept as inspiration. -- Produce an original, flat, print-ready design brief (no garment, no model, no background scene). -- COPYRIGHT-SAFE: no brands, no logos, no characters, no celebrities, no real persons, no franchises. -- AVOID: politics, religion, hate, violence, sexual content, alcohol, national flags. -- motif: English, concrete central subject of the print (e.g. "a smiling cat with a fish", "geometric mountain layers"). -- art_style: English visual technique (e.g. "clean flat vector", "retro screen print"). -- color_palette: English colors (e.g. "sunset orange, cream, dusty blue"). -- composition: English layout (e.g. "centered emblem with balanced negative space"). -- concept: Chinese, one sentence describing the design idea. -- negative_prompt: what to avoid (real people, likeness, characters, logos, text). -- image_prompt: a COMPLETE, fluent English text-to-image prompt for generating the ORIGINAL flat print - design artwork (the print itself, NOT a garment photo). Describe the motif, art style, colors, layout - and mood in natural English, as a standalone print. Do NOT include garment / shirt / model / mannequin / - background-scene / watermark words. Do NOT mention any size or white-background suffix — a fixed - "small centered print on pure white" suffix will be appended automatically by the system. +1. NO COPYING — never reproduce or closely imitate the reference's artwork, + characters, layout or text. Deliberately change motif, arrangement and/or + palette so the two read as clearly different works sharing only a general + style. Distill inspiration into generic style words (retro, y2k, minimal, + grunge, boho, kawaii...); never imitate an identifiable artist/studio/IP + style. +2. FORBIDDEN — brand logos, trademarks, slogans, mascots, copyrighted + characters, real people/celebrities, movie/game/anime/band IP, lyrics, + even stylized or silhouette versions. Avoid politics, religion, violence, + sexual content, alcohol, drugs, gambling, flags, death/occult themes. +3. FORM — ONE clear central subject with strong graphic composition; + print-ready standalone artwork. ANY colors are fine — rich palettes, + gradients and detailed shading are all acceptable. Photographic references + may be rendered as detailed full-color illustrations, retro badges or + vintage stickers. -Return JSON with the field "designs" (array of objects with keys: -motif, art_style, color_palette, composition, concept, negative_prompt, image_prompt).""" +TEXT: short ORIGINAL English wording (1-6 words) allowed; wrap exact words in +double quotes and demand exact spelling; integrate into composition. Never +reuse/translate reference text; no brand/band/movie names or famous slogans. +When unsure, omit. + +suitable_for_print: DEFAULT TRUE for graphics, illustrations, badges, vector +art, typography posters, or prints on mockups (judge only the printed artwork). +FALSE only for: subjectless photo scenery, memes/screenshots/collages, +watermarked or very low-quality images, decor/food/candid photos with no +usable motif. Even when FALSE, still fill all fields so downstream never breaks. + +image_prompt = two parts: + 1) mandatory opener, e.g.: "Use the attached reference image only as loose + inspiration for overall mood, theme and era — do NOT reproduce, trace, + rearrange, recolor or closely imitate any element, character, layout or + text shown in it." + 2) the new design: [central motif] + [style] + [color treatment] + + [composition] + [mood], plus quoted original text if used. + NEVER mention shirts, apparel, models, scenes, sizes, backgrounds or + watermarks — placement is handled externally. + +negative_prompt: copy of reference artwork, likenesses, characters, logos, +trademarks, watermark, photorealistic shirt/product mockups, busy background; +add garbled-lettering terms only if your design includes text. + +OUTPUT — ONLY valid JSON, no fences: +{"designs":[{"suitable_for_print":,"negative_prompt":"","image_prompt":""}]}""" PINTEREST_ANALYZE_SCHEMA = { "name": "pinterest_design_briefs", @@ -354,17 +379,11 @@ PINTEREST_ANALYZE_SCHEMA = { "items": { "type": "object", "properties": { - "image_index": {"type": "integer"}, - "motif": {"type": "string"}, - "art_style": {"type": "string"}, - "color_palette": {"type": "string"}, - "composition": {"type": "string"}, - "concept": {"type": "string"}, + "suitable_for_print": {"type": "boolean"}, "negative_prompt": {"type": "string"}, "image_prompt": {"type": "string"}, }, - "required": ["image_index", "motif", "art_style", "color_palette", - "composition", "concept", "negative_prompt", "image_prompt"], + "required": ["suitable_for_print", "negative_prompt", "image_prompt"], "additionalProperties": False, }, } @@ -375,18 +394,10 @@ PINTEREST_ANALYZE_SCHEMA = { } -def build_pinterest_analyze_user_prompt(term: str, country: str, image_count: int) -> str: +def build_pinterest_analyze_user_prompt() -> str: return ( - f"Country: {country}\n" - f"Pinterest search term: {term}\n" - f"Reference images attached: {image_count} images.\n\n" - f"Analyze the attached images and produce {image_count} ORIGINAL design briefs " - f"(one per image), each capturing the visual vibe as an original T-shirt print design. " - f"Do NOT copy the images.\n" - f"For EACH brief you MUST set image_index to the 0-based position of the input image " - f"it was derived from (first image = 0, second = 1, ...). Every image_index from 0 to " - f"{max(image_count - 1, 0)} must appear exactly once — this links each brief to its " - f"source image so the design is generated from the SAME image that was analyzed." + "Analyze the attached image and produce one ORIGINAL T-shirt print design brief " + "that captures its visual vibe without copying it." ) @@ -587,8 +598,7 @@ class OpenAICompatBackend(LLMBackend): on_400=None) -> List[Dict[str, Any]]: """多模态分析 Pinterest 图片 → 原创设计简报列表。 - 图片输入不被模型支持(纯文本模型 400)时自动降级为纯文本分析(仅用搜索词)。 - 失败返回 [],由节点兜底(回退 mock 规则简报)。 + 图片输入失败/无有效图片时直接放弃(返回 [],不降级纯文本),由节点跳过该产品。 on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。 """ cfg = self._cfg @@ -605,11 +615,26 @@ class OpenAICompatBackend(LLMBackend): data_uris: List[str] = [] for p in image_paths: try: + pk = Path(p) + raw = pk.read_bytes() + # 校验图片完整性:损坏/截断的图片会被火山方舟等多模态接口直接 400 拒绝, + # 必须滤掉后才能编码 base64(PIL 打开失败即视为损坏)。 + if not raw or len(raw) < 100: + print(f"[pinterest_analyze] 图片文件过小/为空,跳过: {p} ({len(raw)}B)") + continue + try: + from PIL import Image + _im = Image.open(pk) + _im.verify() # 校验文件头/结构,不完整则抛异常 + _im.close() + except Exception as _ve: # noqa: BLE001 + print(f"[pinterest_analyze] 图片损坏/不完整,跳过: {p} ({_ve})") + continue import base64 as b64 mime = "image/png" - if Path(p).suffix.lower() in (".jpg", ".jpeg"): + if pk.suffix.lower() in (".jpg", ".jpeg"): mime = "image/jpeg" - data_uris.append(f"data:{mime};base64,{b64.b64encode(Path(p).read_bytes()).decode()}") + data_uris.append(f"data:{mime};base64,{b64.b64encode(raw).decode()}") except Exception as e: # noqa: BLE001 print(f"[pinterest_analyze] 图片读取失败 {p}: {e}") @@ -623,12 +648,11 @@ class OpenAICompatBackend(LLMBackend): except Exception: # noqa: BLE001 pass - def _call(use_images: bool) -> str: + def _call() -> str: user_content: List[Any] = [ - {"type": "text", "text": build_pinterest_analyze_user_prompt(term, country, len(data_uris))}, + {"type": "text", "text": build_pinterest_analyze_user_prompt()}, ] - if use_images: - user_content += [{"type": "image_url", "image_url": {"url": u}} for u in data_uris] + user_content += [{"type": "image_url", "image_url": {"url": u}} for u in data_uris] payload = { "model": model, "messages": [ @@ -656,19 +680,15 @@ class OpenAICompatBackend(LLMBackend): resp.raise_for_status() return str(resp.json()["choices"][0]["message"].get("content") or "") - raw = "" - if data_uris: - try: - raw = _call(use_images=True) - except Exception as e: # noqa: BLE001 纯文本模型不支持图片 → 降级纯文本 - print(f"[pinterest_analyze] 图片输入失败,降级纯文本分析: {e}") - raw = "" - if not raw: - try: - raw = _call(use_images=False) - except Exception as e: # noqa: BLE001 - print(f"[pinterest_analyze] 分析失败: {e}") - return [] + # 图片输入失败/无有效图片 → 直接放弃该产品(不降级纯文本),由节点跳过后续流程 + if not data_uris: + print("[pinterest_analyze] 无有效图片输入,放弃该产品(不降级纯文本)") + return [] + try: + raw = _call() + except Exception as e: # noqa: BLE001 + print(f"[pinterest_analyze] 图片分析失败,放弃该产品(不降级纯文本): {e}") + return [] try: parsed = _extract_json(raw) except Exception as e: # noqa: BLE001 @@ -687,11 +707,7 @@ class OpenAICompatBackend(LLMBackend): continue designs.append({ "topic": term, - "concept": str(d.get("concept", "")).strip(), - "motif": str(d.get("motif", "")).strip(), - "art_style": str(d.get("art_style", "")).strip(), - "color_palette": str(d.get("color_palette", "")).strip(), - "composition": str(d.get("composition", "")).strip(), + "suitable_for_print": bool(d.get("suitable_for_print", True)), "negative_prompt": str(d.get("negative_prompt", "")).strip(), "image_prompt": str(d.get("image_prompt", "")).strip(), # 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应) diff --git a/graph/nodes/compose_node.py b/graph/nodes/compose_node.py index 20f5438..74845cd 100644 --- a/graph/nodes/compose_node.py +++ b/graph/nodes/compose_node.py @@ -12,7 +12,7 @@ import time from pathlib import Path from typing import Any, Dict, List, Optional -from graph.validate import with_fallback +from graph.validate import ThreadSafeErrors, with_fallback RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"} @@ -29,6 +29,18 @@ def _notify_400(on_400, exc) -> None: pass +def _notify_503(on_503, exc) -> None: + """致命图像服务错误(503 / 账户不可用)时触发 on_503 回调(供调用方提前终止任务)。""" + if on_503 is None: + return + try: + from graph.pinterest_pipeline import PinterestPipeline + if PinterestPipeline.is_fatal_503(exc): + on_503() + except Exception: # noqa: BLE001 + pass + + def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str: lines = [ "# POD 印花设计简报(LLM 合规筛选 + 生图提示词)", @@ -112,6 +124,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str, errors: List[Dict[str, Any]] = None, seed: Optional[int] = None, on_400=None, + on_503=None, size: str = "1024x1024") -> Optional[str]: """生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。 @@ -123,6 +136,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str, 无参考图或图生图失败 → 回退 ib.generate() 纯文生图。 seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。 on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。 + on_503: 致命图像服务错误(503/账户不可用)时回调(供调用方提前终止任务)。 """ try: from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint @@ -145,6 +159,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str, except Exception as e: # noqa: BLE001 print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}") _notify_400(on_400, e) + _notify_503(on_503, e) out_path = ib.generate( img_prompt, str(design_dir / f"{out_stem}_design.png"), brief.get("composite_negative", ""), size=size, seed=seed) @@ -161,6 +176,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str, return out_path except Exception as e: # noqa: BLE001 _notify_400(on_400, e) + _notify_503(on_503, e) if errors is not None: errors.append({"node": "compose", "type": type(e).__name__, "message": f"设计稿生成失败 {brief.get('topic','')}: {e}", "trace": ""}) @@ -238,12 +254,13 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]: def _gen_one(i: int, b: Dict[str, Any]): """单张设计稿生成(并发线程内调用,每设计一线程)。""" out_path = generate_design(ib, b, design_dir, f"{country}_{i:02d}", - state.get("errors"), seed=_seed, + _safe_errors, seed=_seed, size=compose_cfg.get("design_size", "1024x1024")) if out_path is None: return i, b, None, None return i, b, out_path, None + _safe_errors = ThreadSafeErrors() targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)] # 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待 workers = max(1, min(len(targets), int((config.get("compose") or {}).get("design_workers", 5)))) @@ -263,6 +280,9 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]: b["design_path"] = out_path designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path}) print(f"[compose] 印花设计稿已生成: {out_path}") + # 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加) + if len(_safe_errors): + state.setdefault("errors", []).extend(list(_safe_errors)) stats = dict(state.get("stats") or {}) stats["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)} diff --git a/graph/nodes/fetch_node.py b/graph/nodes/fetch_node.py index 6025472..27107fa 100644 --- a/graph/nodes/fetch_node.py +++ b/graph/nodes/fetch_node.py @@ -7,6 +7,8 @@ 1. 先用种子词走数据源抓取(Google Trends 等),成功即用新数据; 2. 抓取失败/无结果才回退 output/<国>/collected_keywords.json 旧缓存,保证流水线不中断。 """ +import json +from pathlib import Path from typing import Any, Dict, List from graph.sources import get_source @@ -44,11 +46,9 @@ def fetch_node(state: Dict[str, Any]) -> Dict[str, Any]: use_collected = (config.get("fetch") or {}).get("use_collected", True) if use_collected: try: - import json as _json - from pathlib import Path as _Path - p = _Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json" + p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json" if p.exists(): - data = _json.loads(p.read_text(encoding="utf-8")) + data = json.loads(p.read_text(encoding="utf-8")) cached_rows = data.get("keywords") or [] if cached_rows: rows = [dict(r) for r in cached_rows] # 已过滤去重的关键词 diff --git a/graph/nodes/filter_node.py b/graph/nodes/filter_node.py index e59008a..327e009 100644 --- a/graph/nodes/filter_node.py +++ b/graph/nodes/filter_node.py @@ -5,6 +5,9 @@ 2) 真实人物(名单 + Firstname Lastname 模式,仅对 gt_trending 源,避免误删风格词) 3) 设计相关性(剔除泛新闻/科技/赛事词) """ +import json +import time +from pathlib import Path from typing import Any, Dict, List from graph.scoring import apply_blacklist, filter_design_relevance, filter_person_names, filter_query_noise @@ -70,15 +73,13 @@ def filter_node(state: Dict[str, Any]) -> Dict[str, Any]: # 而不是只显示简报(design_briefs 仅含本次限量生成的热点)。 if kept: try: - import json as _json - from pathlib import Path as _Path - p = _Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json" + p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "collected_keywords.json" p.parent.mkdir(parents=True, exist_ok=True) kws = [{"topic": r.get("topic", ""), "source": r.get("source", ""), "kind": r.get("kind", ""), "raw_score": r.get("raw_score")} for r in kept] - p.write_text(_json.dumps({"country": country, - "collected_at": _datetime_now(), - "keywords": kws}, ensure_ascii=False, indent=2), + p.write_text(json.dumps({"country": country, + "collected_at": _datetime_now(), + "keywords": kws}, ensure_ascii=False, indent=2), encoding="utf-8") print(f"[filter] 已写入采集缓存 {len(kws)} 条(collected_keywords.json)") except Exception as e: # noqa: BLE001 @@ -88,5 +89,4 @@ def filter_node(state: Dict[str, Any]) -> Dict[str, Any]: def _datetime_now() -> str: - import time return time.strftime("%Y-%m-%dT%H:%M:%S") diff --git a/graph/nodes/pinterest_analyze_node.py b/graph/nodes/pinterest_analyze_node.py index 3afc666..2f4e859 100644 --- a/graph/nodes/pinterest_analyze_node.py +++ b/graph/nodes/pinterest_analyze_node.py @@ -3,16 +3,16 @@ 图池机制: - 从持久化图池(image_pool.json)取「未消费」图片(md5 不在 used_images.json)。 - 大图先压缩(内存占用过大 → 缩放/重编码)再送 LLM。 -- 多并发分析(每批 analyze_per_term 张,并发 analyze_concurrency 线程)。 +- 多并发分析(每批 1 张,并发 analyze_concurrency 线程;一次 API 请求一张图,利于 LLM 注意力)。 - 每张被分析的图片 md5 一律拉黑(used_images.json)——合适→产出简报→生成设计(设计 md5 全局拉黑见 compose); 不合适→图片 md5 已拉黑→下一轮自动取下一张,不重复分析。 - 图池无未消费图片时返回空,由路由触发新一轮搜索。 -兜底链:LLM 多模态 → 纯文本降级(后端内部)→ mock 规则简报 → 空列表(下游跳过)。 +兜底链:LLM 多模态分析 → 失败/无图直接放弃本轮(不降级纯文本、不做 mock 兜底)。 带 with_fallback:任何异常都不中断。 """ import concurrent.futures -import re +import uuid from pathlib import Path from typing import Any, Dict, List @@ -25,25 +25,14 @@ from graph.pinterest import ( pool_unused_images, save_used_images, ) -from graph.validate import with_fallback - -# 明显不适合 T 恤印花的简报主体(启发式过滤;真实判定交给 LLM 搜索词引导) -_BRIEF_UNSUITABLE = re.compile( - r"\b(landscape|panorama|scenery|cityscape|street scene|interior|room decor|" - r"food photography|meal|dinner plate|recipe|makeup|nails|manicure|" - r"weather forecast|map|directions|photorealistic scene|realistic portrait)\b", - re.IGNORECASE, -) +from graph.validate import ThreadSafeErrors, with_fallback def _brief_suitable(b: Dict[str, Any]) -> bool: - """简报是否适合做 T 恤印花:非侵权(blocked 拦截)+ 有主体 + 非明显非印花概念。""" + """简报是否适合做 T 恤印花:非侵权(blocked 拦截)+ LLM 判定适合印花(suitable_for_print)。""" if str(b.get("risk_level") or "").strip().lower() == "blocked": return False - motif = str(b.get("motif") or "").strip() - if not motif: - return False - if _BRIEF_UNSUITABLE.search(motif): + if not bool(b.get("suitable_for_print", True)): return False return True @@ -75,25 +64,21 @@ def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str, n = seen_topics.get(base.lower(), 0) seen_topics[base.lower()] = n + 1 topic = base if n == 0 else f"{base} #{n + 1}" - motif = str(b.get("motif") or "").strip() or term - if not motif: - continue + if not bool(b.get("suitable_for_print", True)): + continue # LLM 判定不适合做印花 → 丢弃(md5 已拉黑,下轮取新图) out.append({ "country": country, "topic": topic, "risk_level": str(b.get("risk_level") or "safe").strip().lower() or "safe", "safe_for_print": bool(b.get("safe_for_print", True)), - "suitable_for_print": bool(b.get("suitable_for_print", True)), + "suitable_for_print": True, "design_category": classify(term), - "concept": str(b.get("concept") or "").strip() or f"围绕「{term}」的原创印花设计", - "motif": motif, - "art_style": str(b.get("art_style") or "").strip(), - "color_palette": str(b.get("color_palette") or "").strip(), - "composition": str(b.get("composition") or "").strip(), + "concept": f"围绕「{term}」的原创印花设计", "negative_prompt": str(b.get("negative_prompt") or "").strip(), "image_prompt": str(b.get("image_prompt") or "").strip(), "ref_images": [str(p) for p in (b.get("ref_images") or []) if str(p)], "source_md5": str(b.get("source_md5") or "").strip().lower(), + "brief_id": str(b.get("brief_id") or "").strip(), "slogan": "", "score": 1.0, "confidence": 1.0, @@ -110,23 +95,25 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]: errors = list(state.get("errors") or []) pcfg = config.get("pinterest") or {} - analyze_per_term = int(pcfg.get("analyze_per_term", 1)) + analyze_per_term = 1 # 固定一次 API 请求分析 1 张图(利于 LLM 注意力;不再用 analyze_per_term 配置) concurrency = int(pcfg.get("analyze_concurrency", 3)) max_designs = int(pcfg.get("max_designs", 10)) n_ref = max(1, int(pcfg.get("ref_images_per_design", 1))) provider = str(pcfg.get("provider") or "openai").strip().lower() - # 按需分析:只取补齐到目标所需的图片数(batch_size 为上限,不超额分析),并按 md5 去重, - # 保证同一图片内容(md5)不会同时被多条简报使用 - target = int(state.get("pinterest_target") or 0) - existing = state.get("briefs") or [] - remaining = max(0, target - len(existing)) + # 简报池还有待处理/在途简报 → 不分析新图(等后台消化完再由路由决定下一步) + pipe = state.get("pinterest_pipeline") + if pipe is not None and hasattr(pipe, "pending_count") and pipe.pending_count() > 0: + print(f"[pinterest_analyze] 简报池还有 {pipe.pending_count()} 条在途,本轮不分析") + return {"pinterest_briefs": [], "briefs": state.get("briefs") or [], + "errors": errors} + + # 按需分析:一次把图池当前未消费图片全部分析成简报(搜集完一批→生成一批简报), + # 不再按目标任务数截断(不够用了才由路由触发新采集)。analyze_batch 可设上限保护配额。 batch_size = int(pcfg.get("analyze_batch", 0)) if batch_size <= 0: - # 自动:一次分析补齐到「目标所需」或「每词简报上限」的较小值(每张图→1条简报), - # 让 pipeline 队列一次有足够任务,时刻保持并发生成(避免每轮只推 6 条导致线程空转) - batch_size = min(remaining, max_designs) - need = min(batch_size, remaining) if remaining > 0 else 0 + batch_size = max_designs # 自动:一次最多分析 max_designs 张(每张图→1条简报) + need = batch_size # 1) 图池取未消费图片(md5 不在 used_images);无 → 返回空,路由触发搜索 pool = load_image_pool(output_dir, country) @@ -137,10 +124,6 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]: return {"pinterest_briefs": [], "briefs": state.get("briefs") or [], "errors": errors} - if need <= 0: - print("[pinterest_analyze] 简报已达标,无需分析") - return {"pinterest_briefs": [], "briefs": state.get("briefs") or [], - "errors": errors} seen_md5: set = set() batch: List[Dict[str, Any]] = [] for img in unused: @@ -148,34 +131,27 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]: if m and m in seen_md5: continue # 同一图片内容(md5)不重复分析 seen_md5.add(m) + img["brief_id"] = str(uuid.uuid4()) # md5 校验后分配全局唯一 id,用于图-简报对应校验 batch.append(img) if len(batch) >= need: break - print(f"[pinterest_analyze] 图池取 {len(batch)} 张未消费图片分析(按需 {need}," + print(f"[pinterest_analyze] 图池取 {len(batch)} 张未消费图片分析(本批上限 {need}," f"池剩余未消费 {len(unused) - len(batch)} 张,已消费 {len(used)} 张)") def _assign_refs(res: List[Dict[str, Any]], chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """按简报的 image_index(LLM 返回)匹配它实际分析的图,写入 source_md5 + ref_images。 + """每批只分析 1 张图:简报按顺序对应 chunk 里唯一一张图,写入 source_md5 + ref_images + brief_id。 - 全局 id 校验:简报必须带 image_index(对应输入第几张图,0-based); - 无 image_index(mock 兜底)→ 回退按顺序;无效/越界/重复 → 丢弃该简报(避免错位)。 - 这样 analyze_per_term 可 >1 一次分析多张图提速,简报仍严格对应各自的图。 + 全局 id 校验:分析前已给每张图分配 uuid4(brief_id),简报回填同一 brief_id, + 保证图-简报严格对应(一次 API 请求一张图,无 image_index 错位问题)。 """ chunk_paths = [img["path"] for img in chunk] chunk_md5s = [str(img.get("md5") or "").strip().lower() for img in chunk] - used_idx: set = set() + chunk_ids = [str(img.get("brief_id") or "") for img in chunk] out: List[Dict[str, Any]] = [] for i, b in enumerate(res): if not isinstance(b, dict): continue - try: - idx = int(b.get("image_index")) - except (TypeError, ValueError): - idx = i # 无 image_index → 回退按顺序 - if idx < 0 or idx >= len(chunk_paths) or idx in used_idx: - print(f"[pinterest_analyze] 简报 image_index={idx} 无效/重复,丢弃(避免图-简报错位)") - continue - used_idx.add(idx) + idx = i if i < len(chunk_paths) else 0 # 一次一张:简报按顺序对应唯一图 refs: List[str] = [] for k in range(n_ref): src = chunk_paths[(idx + k) % len(chunk_paths)] @@ -183,6 +159,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]: refs.append(src) b["ref_images"] = refs b["source_md5"] = chunk_md5s[idx] if idx < len(chunk_md5s) else "" + b["brief_id"] = chunk_ids[idx] if idx < len(chunk_ids) else "" out.append(b) return out @@ -210,6 +187,7 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]: # 4) 多并发分析(每线程分析一个 chunk;LLM 后端只读 self._cfg,线程安全) raw_briefs: List[Dict[str, Any]] = [] + _safe_errors = ThreadSafeErrors() def _analyze_chunk(chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]: term = str(chunk[0].get("term") or "") @@ -225,33 +203,27 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]: # 把每条简报的来源图路径回填为原始图(压缩图仅用于分析,参考图用原图) return _assign_refs(res, chunk) except Exception as e: # noqa: BLE001 - errors.append({"node": "pinterest_analyze", "type": type(e).__name__, - "message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""}) + _safe_errors.append({"node": "pinterest_analyze", "type": type(e).__name__, + "message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""}) print(f"[pinterest_analyze] 「{term}」分析失败: {e}") return [] return [] workers = max(1, min(concurrency, len(chunks))) if len(chunks) > 1: - print(f"[pinterest_analyze] 并发分析 {len(chunks)} 批({workers} 线程,每批 {analyze_per_term} 张)…") + print(f"[pinterest_analyze] 并发分析 {len(chunks)} 批({workers} 线程,每批 1 张)…") with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as _ex: _futs = [_ex.submit(_analyze_chunk, c) for c in chunks] for _f in concurrent.futures.as_completed(_futs): raw_briefs.extend(_f.result()) + # 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加) + if len(_safe_errors): + errors.extend(list(_safe_errors)) - # 5) 兜底:LLM 无结果 → mock 规则简报(零 API 成本,保证有设计可生成) + # 5) 真实 LLM 图片分析无结果(失败/无有效图片)→ 直接放弃本轮,跳过后续流程 + # 不再 mock 兜底生成设计(用户要求:分析失败即放弃该产品) if not raw_briefs: - try: - mock = get_backend("mock") - for chunk in chunks: - term = str(chunk[0].get("term") or "") - paths = [compressed_map.get(img["path"], img["path"]) for img in chunk] - res = mock.analyze_pinterest_images(paths, term, country) or [] - _assign_refs(res, chunk) - raw_briefs.extend(res) - print(f"[pinterest_analyze] 兜底:mock 规则简报 {len(raw_briefs)} 条") - except Exception as e: # noqa: BLE001 - print(f"[pinterest_analyze] mock 兜底失败: {e}") + print("[pinterest_analyze] 图片分析无结果,放弃本轮简报(跳过后续流程)") # 6) 本批所有图片 md5 一律拉黑(已消费,不再复用)——合适/不合适都拉黑 for img in batch: @@ -267,19 +239,20 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]: if len(kept) < len(raw_briefs): print(f"[pinterest_analyze] 简报过滤:{len(raw_briefs)} → {len(kept)} 条适合印花") - # 8) 上限 + 去重(同 motif+style 指纹只留一条) + # 8) 上限 + 去重(同 brief_id 只留一条;brief_id 为每张图 uuid4,天然唯一) kept = kept[:max_designs] seen: set = set() uniq: List[Dict[str, Any]] = [] for b in kept: - fp = f"{str(b.get('motif', '')).strip().lower()}|{str(b.get('art_style', '')).strip().lower()}" - if fp in seen: + fp = str(b.get("brief_id") or b.get("image_prompt") or "").strip().lower() + if not fp or fp in seen: continue seen.add(fp) uniq.append(b) kept = uniq - # 9) 富化 → screened → prompt_node 装配提示词 → 标准 briefs(追加到累计,按需截断到目标数) + # 9) 富化 → screened → prompt_node 装配提示词 → 标准 briefs(追加到累计) + # 不再按目标任务数截断:搜集完一批→生成一批简报,不够用了才由路由触发新采集 existing_topics = [str(b.get("topic", "")).strip() for b in (state.get("briefs") or [])] screened = _enrich_briefs(kept, country, existing_topics) new_briefs: List[Dict[str, Any]] = [] @@ -290,10 +263,6 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]: old_count = len(state.get("briefs") or []) accumulated = list(state.get("briefs") or []) accumulated.extend(new_briefs) - target = int(state.get("pinterest_target") or 0) - if target > 0 and len(accumulated) > target: - accumulated = accumulated[:target] - print(f"[pinterest_analyze] 简报已达目标 {target} 条,截断多余部分") # 推送实际新增且保留的简报到并发生成流水线(简报池):边分析边生成设计/三合一/种草图 pushed = accumulated[old_count:] diff --git a/graph/nodes/pinterest_scrape_node.py b/graph/nodes/pinterest_scrape_node.py index 092439b..87d6503 100644 --- a/graph/nodes/pinterest_scrape_node.py +++ b/graph/nodes/pinterest_scrape_node.py @@ -55,6 +55,31 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]: headless = bool(pcfg.get("headless", False)) proxy = pcfg.get("proxy") or None search_mode = str(pcfg.get("search_mode") or "direct").strip().lower() + login_check = bool(pcfg.get("login_check", True)) + login_wait = bool(pcfg.get("login_wait", False)) + + # 爬取前静态检测 Pinterest 登录态(不启动 Chrome,只读 .chrome_session cookies): + # 未登录/无会话 → 跳过本轮爬取并告警,避免每个搜索词都启动 Chrome 后才发现未登录。 + login_state: Dict[str, Any] = {"status": "unknown"} + if login_check: + try: + from pinterest_scraper.pinterest_image_capture import check_login_state + login_state = check_login_state() + except Exception as e: # noqa: BLE001 + login_state = {"status": "unknown", "detail": f"登录态检测失败: {e}"} + status = login_state.get("status") + if status in ("logged_out", "no_session"): + print(f"[pinterest_scrape] ⚠️ 未检测到 Pinterest 登录态({login_state.get('detail')})。" + f"跳过本轮 {len(terms)} 个搜索词爬取。") + stats = dict(state.get("stats") or {}) + stats["pinterest_scrape"] = { + "terms": len(terms), "scraped": 0, "skipped": 0, "failed": 0, + "images": 0, "pool": len((load_image_pool(output_dir, country).get("images")) or []), + "login_status": status, + } + return {"pinterest_images": {}, "pinterest_attempted": state.get("pinterest_attempted") or [], + "pinterest_login": login_state, "stats": stats, "errors": errors} + print(f"[pinterest_scrape] 登录态检测:{status}({login_state.get('detail')})") # 所有搜索词共享同一个 .chrome_session 登录态目录,Chrome 对同一 user-data-dir 是单例, # 并发启动会互相抢占导致 "browser has been closed",必须串行爬取。 @@ -62,7 +87,8 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]: print(f"[pinterest_scrape] 共享登录态目录不支持并发,scrape_concurrency 强制为 1(原 {concurrency})") concurrency = 1 - # 一次性探测并校验代理(Pinterest 需代理才能访问;代理失效时给出明确警告,避免逐词静默失败) + # 默认走代理:config.pinterest.proxy 未配置时自动探测(环境变量/系统代理/本地常见端口), + # 本地 VPN 已开启时探测到的代理即可访问 Pinterest;代理失效时给出明确警告,避免逐词静默失败。 if proxy is None: try: from pinterest_scraper.pinterest_image_capture import detect_proxy, get_system_proxy, _validate_proxy @@ -91,7 +117,8 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]: try: from pinterest_scraper.scraper import scrape_pinterest files = scrape_pinterest(term, count=images_per_term, - save_dir=str(term_dir), proxy=proxy, headless=headless) + save_dir=str(term_dir), proxy=proxy, headless=headless, + login_wait=login_wait) results[term] = files except Exception as e: # noqa: BLE001 failed.append(term) @@ -150,8 +177,9 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]: stats["pinterest_scrape"] = { "terms": len(terms), "scraped": len(results), "skipped": len(skipped), "failed": len(failed), "images": total, "pool": len(pool.get("images") or []), + "login_status": login_state.get("status", "unknown"), } print(f"[pinterest_scrape] 完成:{len(results)} 个搜索词,共 {total} 张图(跳过 {len(skipped)},失败 {len(failed)})") return {"pinterest_images": results, "pinterest_attempted": attempted, - "stats": stats, "errors": errors} + "pinterest_login": login_state, "stats": stats, "errors": errors} diff --git a/graph/nodes/product_node.py b/graph/nodes/product_node.py index 6b0a42f..42d6faa 100644 --- a/graph/nodes/product_node.py +++ b/graph/nodes/product_node.py @@ -34,22 +34,18 @@ from typing import Any, Dict, List, Optional from graph.paths import project_root, runtime_root from graph.product import ( find_basemap, - find_first_model_folder, first_available_sku, list_colors, list_spus, ) -from graph.validate import with_fallback +from graph.validate import ThreadSafeErrors, with_fallback _USED_LOCK = threading.Lock() # used_designs.json 并发写锁 -_MODEL_LOCK = threading.Lock() # 同款共用模特缓存并发锁 -_MODEL_CACHE: Dict[str, Any] = {} # 同款共用模特:spu_code → model 路径 def _next_img_idx(prod_dir: Path, prefix: str) -> int: """货号续号:扫 prod_dir 已有 {prefix}{数字}* 文件,返回下一个起始序号(不覆盖旧产物)。""" - import re max_n = -1 try: if prod_dir.exists(): @@ -116,17 +112,30 @@ def _template_out_path(prod_dir: Path, chosen_sku: str) -> Path: return prod_dir / f"{chosen_sku}_已填写_{int(time.time())}.xlsx" +def _is_fatal_50x(e) -> bool: + """致命图像服务错误(503 / No available compatible accounts)→ 不重试,提前终止。""" + try: + from graph.pinterest_pipeline import PinterestPipeline + return PinterestPipeline.is_fatal_503(e) + except Exception: # noqa: BLE001 + return False + + def _retry_image(fn, *args, attempts: int = 3, backoff=(5, 20, 40), **kwargs): - """图像合成带退避重试(网关超载/超时常见):成功返回 out_path;全部失败返回 None。""" - import time as _t + """图像合成带退避重试(网关超载/超时常见):成功返回 out_path;全部失败返回 None。 + + 致命 503(账户不可用)重试无效 → 直接抛出,交由调用方终止任务。 + """ last = None for i in range(attempts): try: return fn(*args, **kwargs) except Exception as e: # noqa: BLE001 + if _is_fatal_50x(e): + raise last = e if i < attempts - 1: - _t.sleep(backoff[i]) + time.sleep(backoff[i]) print(f"[product] 图像合成重试 {attempts} 次均失败: {last}") return None @@ -135,16 +144,31 @@ def _process_spu( db_path, basemap_root, material_root, category, prod_dir, brief, ib, spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="", img_code="", model_img=None, design_size="1024x1024", compose_size="1536x2048", + on_503=None, ) -> Optional[Dict[str, Any]]: """处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。 shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。 img_code: 货号(前缀+3位计数);本产品所有图片文件归入 prod_dir/{img_code}/ 子文件夹 (按货号命名,包含该货号对应的所有图片)。 + on_503: 致命图像服务错误(503/账户不可用)回调(供调用方提前终止任务)。 返回 result dict;内部异常已兜底,不中断。 """ # 输出目录 = 货号子文件夹(product/DG000/…),该货号所有图片都放这里 prod_dir = prod_dir / img_code prod_dir.mkdir(parents=True, exist_ok=True) + + def _fatal(e) -> bool: + """致命图像服务错误(503/账户不可用)→ 通知 on_503 并返回 True(调用方应立即终止)。""" + try: + from graph.pinterest_pipeline import PinterestPipeline + if PinterestPipeline.is_fatal_503(e): + if on_503 is not None: + on_503() + return True + except Exception: # noqa: BLE001 + pass + return False + colors = list_colors(db_path, spu["code"]) valid_codes = {c["sku_code"] for c in colors} if sku_code: @@ -220,6 +244,9 @@ def _process_spu( result["design_from"] = "product" print(f"{tag} 纯印花设计稿已生成(product 节点): {design_path}") except Exception as e: # noqa: BLE001 + if _fatal(e): + print(f"{tag} 图像服务 503,终止: {e}") + return None errors.append({"node": "product", "type": type(e).__name__, "message": f"设计稿生成失败: {e}", "trace": ""}) print(f"{tag} 设计稿生成失败: {e}") @@ -262,17 +289,26 @@ def _process_spu( print(f"{tag} 三图模特合成图已生成(耗时 {int(time.time()-t0)}s): {composite_path}") except Exception as e: # noqa: BLE001 # 合成失败 → 带退避重试(网关超载/超时常见,重试 3 次) - print(f"{tag} 三图合成失败,退避重试…: {e}") - retried = _retry_image(ib.print, wear_prompt, str(model_img), composite_path, - brief.get("composite_negative", ""), - extra_images=[design_path, str(basemap_img)], size=compose_size) - if retried is not None: - result["composite_path"] = composite_path - print(f"{tag} 三图合成重试成功(耗时 {int(time.time()-t0)}s): {composite_path}") - else: - errors.append({"node": "product", "type": type(e).__name__, "message": f"模特合成失败(重试仍失败): {e}", "trace": ""}) - print(f"{tag} 三图合成重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}") + if _fatal(e): + print(f"{tag} 图像服务 503,终止: {e}") return None + print(f"{tag} 三图合成失败,退避重试…: {e}") + try: + retried = _retry_image(ib.print, wear_prompt, str(model_img), composite_path, + brief.get("composite_negative", ""), + extra_images=[design_path, str(basemap_img)], size=compose_size) + if retried is not None: + result["composite_path"] = composite_path + print(f"{tag} 三图合成重试成功(耗时 {int(time.time()-t0)}s): {composite_path}") + else: + errors.append({"node": "product", "type": type(e).__name__, "message": f"模特合成失败(重试仍失败): {e}", "trace": ""}) + print(f"{tag} 三图合成重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}") + return None + except Exception as e2: # noqa: BLE001 + if _fatal(e2): + print(f"{tag} 图像服务 503(重试时),终止: {e2}") + return None + raise else: # mark=1 无模特图 → 统一只做三合一,不做印花+底图两图合成 if int(spu.get("mark") or 0) == 1: @@ -289,17 +325,26 @@ def _process_spu( result["printed_path"] = printed_path print(f"{tag} 平铺服装图已生成(无模特,底图+印花): {printed_path}") except Exception as e: # noqa: BLE001 - print(f"{tag} 平铺服装图失败,退避重试…: {e}") - retried = _retry_image(ib.print, flat_prompt, str(basemap_img), printed_path, - brief.get("composite_negative", ""), - extra_images=[design_path], size=compose_size) - if retried is not None: - result["printed_path"] = printed_path - print(f"{tag} 平铺服装图重试成功: {printed_path}") - else: - errors.append({"node": "product", "type": type(e).__name__, "message": f"平铺服装图生成失败(重试仍失败): {e}", "trace": ""}) - print(f"{tag} 平铺服装图重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}") + if _fatal(e): + print(f"{tag} 图像服务 503,终止: {e}") return None + print(f"{tag} 平铺服装图失败,退避重试…: {e}") + try: + retried = _retry_image(ib.print, flat_prompt, str(basemap_img), printed_path, + brief.get("composite_negative", ""), + extra_images=[design_path], size=compose_size) + if retried is not None: + result["printed_path"] = printed_path + print(f"{tag} 平铺服装图重试成功: {printed_path}") + else: + errors.append({"node": "product", "type": type(e).__name__, "message": f"平铺服装图生成失败(重试仍失败): {e}", "trace": ""}) + print(f"{tag} 平铺服装图重试仍失败 → 跳过该产品(不生成标题/不写模板): {e}") + return None + except Exception as e2: # noqa: BLE001 + if _fatal(e2): + print(f"{tag} 图像服务 503(重试时),终止: {e2}") + return None + raise # 7.2) 多色:单 SPU 多色时每个颜色再执行一次三合一(用各自底图),轮播图按颜色路由 color_composites: List[Dict[str, Any]] = [] @@ -322,6 +367,9 @@ def _process_spu( color_composites.append({"sku_code": sc, "color": col, "composite_path": cp}) print(f"{tag} 颜色 {sc}({col})三合一已生成: {cp}") except Exception as e: # noqa: BLE001 + if _fatal(e): + print(f"{tag} 颜色 {sc} 三合一遇 503,终止: {e}") + return None errors.append({"node": "product", "type": type(e).__name__, "message": f"颜色 {sc} 三合一失败: {e}", "trace": ""}) result["color_composites"] = color_composites @@ -445,23 +493,25 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]: prod_dir = output_dir / "product" prod_dir.mkdir(parents=True, exist_ok=True) - # 4.1) 任务级模特分配(material_library-): - # 一个 SPU 对应一个模特;SPU(不同款)数 > 模特数 → 从全部模特循环兜底(允许重复) - model_assign: Dict[str, Any] = {} - _all_models: List[str] = [] + # 4.1) 任务级模特分配(按 spu.mark 映射模特目录 → 过滤非 3:4 图片 → 每个任务随机抽取): + # 每个产品任务(含同一 SPU 的多个款)都独立随机抽一个模特,保证同款多产品模特不重复 + model_assign: Dict[int, Any] = {} try: - _folder, _all_models = find_first_model_folder(material_root, category) + from graph.product import build_mark_model_map, find_model_images_for_mark + mark_map = build_mark_model_map(db_path, material_root) except Exception: # noqa: BLE001 - _all_models = [] - if _all_models: - seen_spu: Dict[str, str] = {} - for _i, (_spu, _skus, _tb) in enumerate(worklist): - code = _spu.get("code", "") - if code not in seen_spu: - seen_spu[code] = _all_models[_i % len(_all_models)] # SPU>模特数 → 循环兜底 - model_assign[code] = seen_spu[code] - print(f"[product] 任务级模特分配:{len(seen_spu)} 个 SPU,模特池 {len(_all_models)} 张" - f"{'(SPU>模特,循环兜底)' if len(seen_spu) > len(_all_models) else ''}") + mark_map = {} + for _i, (_spu, _skus, _tb) in enumerate(worklist): + mark = str(_spu.get("mark") or "").strip() or "1" + folder = mark_map.get(mark, category) + try: + pool_imgs = find_model_images_for_mark(db_path, material_root, mark, folder) + except Exception: # noqa: BLE001 + pool_imgs = [] + if pool_imgs: + model_assign[_i] = random.choice(pool_imgs) # 过滤后随机抽(按任务序号) + if model_assign: + print(f"[product] 任务级模特分配:{len(model_assign)} 个产品任务(按 mark 过滤 3:4 后随机抽取)") # 5) compose 节点生成的共享设计稿(图2):每个任务用自己的热点简报设计(tb.design_path), # 一个货号对应一个设计(多颜色共用该设计),不再所有产品共用第一个 @@ -503,8 +553,11 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]: concurrency = int(pcfg.get("concurrency") or 0) or min(len(worklist), 5) print(f"[product] 并发 {concurrency}(每 SPU 一线程,上限 {concurrency})处理 {len(worklist)} 个产品任务") - def _run_one(idx: int, spu, skus, tb): + _safe_errors = ThreadSafeErrors() + + def _run_one(wi: int, spu, skus, tb): """并发执行单个产品:返回 (result or None, img_code)。失败由 _process_spu 内部兜底。""" + idx = start_idx + wi # 实际货号序号(start_idx 起自动续号) img_code = f"{prefix}{idx:03d}" # 货号:图片按此命名(DG000_design.png…) try: # 每个任务用自己的热点设计(designs_map),并拷贝为货号命名(designs/DG000_design.png) @@ -519,9 +572,9 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]: tb = dict(tb) tb["design_path"] = design_path r = _process_spu(db_path, basemap_root, material_root, category, prod_dir, - tb, ib, spu, skus, pcfg, errors, design_path, title_backend, + tb, ib, spu, skus, pcfg, _safe_errors, design_path, title_backend, country, img_code=img_code, - model_img=model_assign.get(spu.get("code", "")), + model_img=model_assign.get(wi), # 按任务序号取独立随机模特 design_size=str((config.get("compose") or {}).get("design_size") or "1024x1024"), compose_size=str((config.get("compose") or {}).get("size") or "1536x2048")) if r: @@ -535,7 +588,7 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]: # 货号自动续号:任务一开始全部按序分配(start_idx 起),不覆盖已生成的产物 start_idx = _next_img_idx(prod_dir, prefix) with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex: - futures = [ex.submit(_run_one, start_idx + i, spu, skus, tb) + futures = [ex.submit(_run_one, i, spu, skus, tb) for i, (spu, skus, tb) in enumerate(worklist)] for f in concurrent.futures.as_completed(futures): r, img_code = f.result() @@ -545,6 +598,9 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]: _record_used(cache_dir, r) # (热点-风格) 去重记录 → 缓存根目录 except Exception: # noqa: BLE001 pass + # 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加) + if len(_safe_errors): + errors.extend(list(_safe_errors)) results.sort(key=lambda x: x.get("img_code", "")) # 按货号排序,模板/清单顺序稳定 _write_products(prod_dir, results) diff --git a/graph/nodes/prompt_node.py b/graph/nodes/prompt_node.py index da55088..a796f92 100644 --- a/graph/nodes/prompt_node.py +++ b/graph/nodes/prompt_node.py @@ -22,6 +22,27 @@ PINTEREST_PRINT_SUFFIX = ( "no garment, no shirt, no model, no mannequin, no watermark" ) +# Pinterest 生图提示词 4 段结构中第 3 段的引导前缀:把 LLM 产出的 negative_prompt +# 转成一条正向「Strictly avoid: ...」条款拼进 image_prompt,让防复制/防商标约束落到生成指令 +NEG_LEAD = "Strictly avoid: " + +# Pinterest 生图提示词 4 段结构中第 4 段(仅当设计含文字时追加): +# 要求模型把引号内的文字按原文逐字正确拼写,避免乱码/拼错 +SPELLING_RULE = ( + "Render every phrase shown in quotes exactly as written, " + "correctly spelled." +) + +# review(疑似商标/受保护主题)简报统一追加的「原创化魔改」引导段: +# 只做风格参考,禁复刻品牌/商标/角色,换名换细节,生成通用非侵权致敬式设计。 +# 两个模式(热点采集 / Pinterest 参考)共用同一文本,避免不一致。 +REVIEW_REBRAND_HINT = ( + "IMPORTANT: this theme is ONLY a loose stylistic reference. " + "Do NOT reproduce any brand logo, trademark, character, mascot, copyrighted artwork or real person. " + "Create a fully ORIGINAL design with a different name and distinct visual details and colors — " + "a generic, non-infringing homage in the same mood, clearly distinct from the original." +) + # 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif, # 避免 gpt-image 等内容策略频繁拦截导致"生图限制多") _IMG_RISKY_SWAP = { @@ -73,27 +94,32 @@ def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]: composition = (r.get("composition") or derive_composition(r["topic"], r.get("design_category"))).strip() prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country) - # Pinterest 简报:直接用 LLM 多模态对图片的描述拼接的 image_prompt(跳过四要素模板), - # 仅追加统一的「小印花 + 纯白底」约束段;wearable/composite 仍用模板装配。 llm_ip = (r.get("image_prompt") or "").strip() + llm_neg = (r.get("negative_prompt") or "").strip() if r.get("source") == "pinterest" and llm_ip: - prompts["image_prompt"] = llm_ip + PINTEREST_PRINT_SUFFIX - print(f"[prompt] Pinterest 简报用 LLM 多模态描述作为 image_prompt(跳过四要素模板): 「{r['topic']}」") - # 文字印花(约 30% 概率):简报有 slogan 时,随机注入文字段到设计稿提示词 - slogan = (r.get("slogan") or "").strip() - if slogan and random.random() < float(config.get("prompt_templates", {}).get("text_ratio", 0.3)): - text_seg = (f', with the text "{slogan}" rendered as bold retro typography, ' - f'lettering clean and correctly spelled, high contrast, as the focal text of the print') - prompts["image_prompt"] = prompts["image_prompt"] + text_seg - r["used_slogan"] = slogan - # review(疑似商标/受保护主题)→ 动态注入「原创化魔改」引导:只做风格参考,禁止复刻品牌/商标/角色, - # 换名换细节,生成通用非侵权的致敬式设计 + # —— Pinterest 参考模式:跳过四要素模板,按 4 段结构拼 image_prompt —— + # ① image_prompt(分析模型产出) + 固定输出形态后缀 PINTEREST_PRINT_SUFFIX + # ② 负向条款(由 LLM negative_prompt 经 NEG_LEAD 引导,转化进正向指令) + # ③ 拼写锁定句 SPELLING_RULE(仅当 LLM image_prompt 已含引号文字段时) + # 是否含文字、拼写与否均由分析模型产出决定,本模式不注入 slogan。 + seg: List[str] = [llm_ip, PINTEREST_PRINT_SUFFIX.strip()] + if llm_neg: + seg.append(NEG_LEAD + llm_neg) + if '"' in llm_ip: + seg.append(SPELLING_RULE) + prompts["image_prompt"] = ", ".join(seg) + print(f"[prompt] Pinterest 简报按 4 段结构拼 image_prompt(跳过四要素模板): 「{r['topic']}」") + else: + # —— 热点采集模式:四要素模板装配 + 文字印花(约 30% 概率注入 slogan)—— + slogan = (r.get("slogan") or "").strip() + if slogan and random.random() < float(config.get("prompt_templates", {}).get("text_ratio", 0.3)): + text_seg = (f', with the text "{slogan}" rendered as bold retro typography, ' + f'lettering clean and correctly spelled, high contrast, as the focal text of the print') + prompts["image_prompt"] = prompts["image_prompt"] + text_seg + r["used_slogan"] = slogan + # review(疑似商标/受保护主题)→ 追加「原创化魔改」引导(两个模式通用) if str(r.get("risk_level", "")).strip().lower() == "review": - prompts["image_prompt"] = (prompts["image_prompt"] - + " IMPORTANT: this theme is ONLY a loose stylistic reference. " - "Do NOT reproduce any brand logo, trademark, character, mascot, copyrighted artwork or real person. " - "Create a fully ORIGINAL design with a different name and distinct visual details and colors — " - "a generic, non-infringing homage in the same mood, clearly distinct from the original.") + prompts["image_prompt"] = prompts["image_prompt"] + " " + REVIEW_REBRAND_HINT print(f"[prompt] review 简报注入原创化魔改引导: 「{r['topic']}」") r.update(prompts) r["motif"] = motif diff --git a/graph/nodes/template_export_node.py b/graph/nodes/template_export_node.py index 5b54007..6e71ed9 100644 --- a/graph/nodes/template_export_node.py +++ b/graph/nodes/template_export_node.py @@ -20,14 +20,8 @@ from graph.validate import with_fallback def _template_out_path(prod_dir: Path, tpl_name: str) -> Path: """模板输出路径:默认 {tpl_name}_已填写.xlsx;已存在/被占用则自动换名加序号(同款号多产品不互相覆盖)。""" base = prod_dir / f"{tpl_name}_已填写.xlsx" - try: - with open(base, "ab"): - pass - except OSError: - pass - else: - if not base.exists(): - return base + if not base.exists(): + return base for i in range(2, 100): cand = prod_dir / f"{tpl_name}_已填写_{i}.xlsx" if not cand.exists(): @@ -95,6 +89,7 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]: sku_codes = [r.get("sku_code") or ""] batch.append({ "spu_code": r.get("spu_code", ""), + "img_code": r.get("img_code", ""), "sku_codes": sku_codes, "images": [], "spu_per_color": True, # 每颜色一个独立 SPU 块(单色多 SPU) @@ -107,13 +102,24 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]: "seed_shot_urls": r.get("seed_shot_urls") or [], }) + # 写入模板前按货号(前端自定义前缀+3位计数,如 DG001)最后3位从小到大排序,按顺序插入 + def _tail_num(rec: Dict[str, Any]) -> tuple: + code = str(rec.get("img_code") or rec.get("oss_code") or "") + try: + return (0, int(code[-3:])) + except ValueError: + return (1, 0) + batch.sort(key=_tail_num) + exported: List[str] = [] if batch: - out = _template_out_path(prod_dir, "商品上传") + # 输出文件名 = 模板原文件名 + _已填写(如 NEW-波兰男黑T恤_已填写.xlsx) + out = _template_out_path(prod_dir, Path(tp).stem) try: out = export_products( db_path, batch, tdir, tp, str(out), markup_percent=float(pcfg.get("markup_percent") or 0), + suggested_price_ratio=float(pcfg.get("suggested_price_ratio") or 0), ) for r in products: if (r.get("composite_path") or r.get("printed_path")) and (r.get("en_title") or "").strip(): diff --git a/graph/oss_upload.py b/graph/oss_upload.py index 17e75c8..feed7b3 100644 --- a/graph/oss_upload.py +++ b/graph/oss_upload.py @@ -88,10 +88,3 @@ def build_oss_key(country: str, timestamp: str, code: str, rand4: str, ext: str """对象 key:{国家}/{时间戳}/{货号}_{4位随机}.jpg(如 GB/20260820150945/DG000_Ab3x.jpg)。""" safe = lambda s: "".join(c for c in (s or "") if c.isalnum() or c in "-_").strip() return f"{safe(country)}/{safe(timestamp)}/{safe(code)}_{safe(rand4)}{ext}" - - -def random_code4() -> str: - """4 位随机:大小写英文 + 数字。""" - import random - import string - return "".join(random.choices(string.ascii_letters + string.digits, k=4)) diff --git a/graph/pinterest_pipeline.py b/graph/pinterest_pipeline.py index af7bec9..19dc84c 100644 --- a/graph/pinterest_pipeline.py +++ b/graph/pinterest_pipeline.py @@ -12,6 +12,7 @@ 并发上限与 product_node 一致(默认 5),避免压垮图像网关。 """ import concurrent.futures +import random import re import threading import time @@ -35,13 +36,23 @@ class PinterestPipeline: self._briefs: List[Dict[str, Any]] = [] self._done = False self._cursor = 0 + # 正在处理(已提交线程池、尚未完成)的简报数,供路由判断简报池是否空闲 + self._in_flight = 0 - # 结果 + # 结果:每完成一个产品立即落盘追加写入 products_pending.jsonl, + # 内存列表仅作缓存(finish 时再读盘合并),中途崩溃也不丢已完成产品。 self._products: List[Dict[str, Any]] = [] self._products_lock = threading.Lock() self._errors: List[Dict[str, Any]] = [] self._errors_lock = threading.Lock() + # 致命图像服务错误(53/账户不可用):置位后终止分发、丢弃未完成简报,仅保留已完成产品 + self._fatal_lock = threading.Lock() + self._fatal_503 = False + + # 已完成产品落盘文件(JSONL 追加写):output///products_pending.jsonl + self._pending_file = self.output_dir / "products_pending.jsonl" + # 路径解析(复用 product_node 的 _abs 逻辑:运行根优先,其次数据根) pcfg = self.config.get("product") or {} @@ -188,19 +199,28 @@ class PinterestPipeline: return None def _assign_models(self) -> Dict[str, Any]: + """任务级模特分配:按 spu.mark 映射模特目录 → 过滤非 3:4 图片 → 每个任务随机抽取。 + + 每个产品任务(含同一 SPU 的多个款)都独立随机抽一个模特,保证同款多产品模特不重复。 + """ model_assign: Dict[str, Any] = {} try: - from graph.product import find_first_model_folder - _folder, _all_models = find_first_model_folder(self._material_root, self._category) + from graph.product import build_mark_model_map, find_model_images_for_mark + mark_map = build_mark_model_map(self._db_path, self._material_root) except Exception: # noqa: BLE001 - _all_models = [] - if _all_models: - seen: Dict[str, str] = {} - for _i, (spu, _skus) in enumerate(self._worklist): - code = spu.get("code", "") - if code not in seen: - seen[code] = _all_models[_i % len(_all_models)] - model_assign[code] = seen[code] + mark_map = {} + # 每个任务按序号绑定独立随机模特(同 SPU 多款也各自随机,不共用) + for _i, (spu, _skus) in enumerate(self._worklist): + key = f"task_{_i}" + mark = str(spu.get("mark") or "").strip() or "1" + folder = mark_map.get(mark, self._category) + try: + pool_imgs = find_model_images_for_mark(self._db_path, self._material_root, + mark, folder) + except Exception: # noqa: BLE001 + pool_imgs = [] + if pool_imgs: + model_assign[key] = random.choice(pool_imgs) # 过滤后随机抽 return model_assign def _load_materials(self) -> Dict[str, str]: @@ -237,6 +257,48 @@ class PinterestPipeline: with self._err400_lock: return self._err400_aborted + # ------------------------------------------------------------------ # + # 致命图像服务错误(503 / 账户不可用):重试无效,提前终止整个任务 + # ------------------------------------------------------------------ # + @staticmethod + def is_fatal_503(exc) -> bool: + """判断异常是否为「图像服务不可用」类致命错误(503 / No available compatible accounts)。 + + 这类错误说明账户配额耗尽或网关故障,重试必然失败,应提前终止任务而非无意义重试。 + """ + msg = str(exc) + if "503" in msg: + return True + low = msg.lower() + return "no available compatible accounts" in low or "account" in low and "not available" in low + + def record_503(self) -> bool: + """记录一次致命 503:首次触发即置位终止标志(后续请求直接短路不再提交)。 + + 返回 True 表示本次触发终止(调用方应立即停止当前链路)。 + """ + with self._fatal_lock: + first = not self._fatal_503 + self._fatal_503 = True + if first: + print("[pinterest_pipeline] ⛔ 检测到图像服务 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: """放弃当前种子词:清空其未完成简报 + 图池未消费图片(已完成的保留)。""" term = self._err400_term @@ -298,18 +360,52 @@ class PinterestPipeline: self._cond.notify_all() print(f"[pinterest_pipeline] 简报池 +{len(briefs)} 条(待处理 {len(self._briefs)})") + def pending_count(self) -> int: + """简报池中待处理 + 正在处理的简报数(供路由判断是否需要补图/补分析)。""" + with self._cond: + queued = len(self._briefs) + return queued + self._in_flight + + def wait_idle(self, timeout: Optional[float] = None) -> bool: + """阻塞等待简报池消化完(无待处理且无在途),返回是否已空闲。 + + 用 Condition 等待(_process_one 完成时 notify_all 唤醒),而非轮询 sleep, + 避免 wait 循环疯狂刷屏。timeout 为 None 时无限等待(受 _done 保护)。 + """ + with self._cond: + while (self._briefs or self._in_flight > 0) and not self._done: + if timeout is not None: + deadline = time.time() + timeout + remaining = deadline - time.time() + if remaining <= 0: + return False + self._cond.wait(min(remaining, 1.0)) + else: + self._cond.wait() + return not self._briefs and self._in_flight <= 0 + def finish(self) -> tuple: - """排空简报池、等待全部产品完成,返回 (products, errors)。""" + """排空简报池、等待全部产品完成,返回 (products, errors)。 + + 产品来源:手动已完成(内存缓存)+ 落盘文件(products_pending.jsonl) + 按货号去重合并——即使中途 503 终止/崩溃,已完成产品也不丢。 + """ with self._cond: self._done = True self._cond.notify_all() self._dispatcher.join() self._pool.shutdown(wait=True) + # 读盘 + 内存合并去重(内存为准,但以落盘为最终权威——崩溃恢复后走落盘) + pending = self.load_pending() + merged = {str(p.get("img_code", "")): p for p in pending} with self._products_lock: - products = list(self._products) + for p in self._products: + merged[str(p.get("img_code", ""))] = p + products = [merged[k] for k in merged if k] with self._errors_lock: errors = list(self._errors) - print(f"[pinterest_pipeline] 收尾:完成 {len(products)} 个产品,错误 {len(errors)}") + print(f"[pinterest_pipeline] 收尾:完成 {len(products)} 个产品,错误 {len(errors)}" + f"{'(含落盘恢复 ' + str(len(pending)) + ')' if pending else ''}") return products, errors # ------------------------------------------------------------------ # @@ -322,8 +418,16 @@ class PinterestPipeline: self._cond.wait() if self._done and not self._briefs: break + if self._fatal_503: + # 致命 503:不再分发新简报(未完成的废弃,仅保留已完成落盘产品) + self._briefs = [] + self._done = True + self._cond.notify_all() + break batch = self._briefs self._briefs = [] + # 与 _briefs 清空同一临界区递增在途数,避免 wait_idle 误判空闲 + self._in_flight += len(batch) for b in batch: with self._cond: idx = self._cursor @@ -335,6 +439,8 @@ class PinterestPipeline: # ------------------------------------------------------------------ # def _process_one(self, brief: Dict[str, Any], idx: int) -> None: try: + if self.is_fatal_503_aborted(): + return # 0) 先定货号:整条链路(设计/三合一/种草图)都用它命名与匹配,避免序号错位 if idx >= len(self._worklist): print(f"[pinterest_pipeline] 简报 {idx} 无对应产品任务,跳过") @@ -343,6 +449,8 @@ class PinterestPipeline: img_code = f"{self._prefix}{idx:03d}" # 1) 生成设计(compose)——直接按货号命名 designs/{img_code}_design.png design_path = self._gen_design(brief, img_code) + if self.is_fatal_503_aborted(): + return if self.is_400_aborted(): # 当前种子词 400 超限已放弃:正在生成的当个也放弃,不进入后续链路 print(f"[pinterest_pipeline] 当前种子词 400 超限已放弃,跳过简报 {idx}") @@ -357,13 +465,17 @@ class PinterestPipeline: return brief = new_brief design_path = self._gen_design(brief, img_code) + if self.is_fatal_503_aborted(): + return if design_path: break if not design_path: return brief["design_path"] = design_path - # 2) 三合一(product)——同一货号 - prod = self._process_spu(brief, spu, skus, img_code, design_path) + # 2) 三合一(product)——同一货号;task_idx=本次简报序号,模特按任务独立随机 + prod = self._process_spu(brief, spu, skus, img_code, design_path, task_idx=idx) + if self.is_fatal_503_aborted(): + return if not prod: return # 3) OSS 上传 @@ -376,6 +488,8 @@ class PinterestPipeline: _record_used(self.cache_dir, prod) except Exception: # noqa: BLE001 pass + # 5) 落盘:每完成一个产品立即追加写入 products_pending.jsonl(不依赖内存,崩溃不丢) + self._persist_product(prod) with self._products_lock: self._products.append(prod) print(f"[pinterest_pipeline] 产品完成: {prod.get('img_code', '')}" @@ -385,6 +499,42 @@ class PinterestPipeline: self._errors.append({"node": "pinterest_pipeline", "type": type(e).__name__, "message": f"简报 {idx} 处理失败: {e}", "trace": ""}) print(f"[pinterest_pipeline] 简报 {idx} 处理失败: {e}") + finally: + with self._cond: + self._in_flight = max(0, self._in_flight - 1) + self._cond.notify_all() + + def _persist_product(self, prod: Dict[str, Any]) -> None: + """把已完成产品追加写入 products_pending.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]: if self._ib is None: @@ -398,10 +548,18 @@ class PinterestPipeline: if self.record_400(): self._abort_current_term() + def _on_503(): + self.record_503() + self.abort_unfinished() + return generate_design(self._ib, brief, design_dir, img_code, self._errors, - on_400=_on_400, + on_400=_on_400, on_503=_on_503, size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024")) except Exception as e: # noqa: BLE001 + if self.is_fatal_503(e): + self.record_503() + self.abort_unfinished() + return None with self._errors_lock: self._errors.append({"node": "compose", "type": type(e).__name__, "message": f"设计稿生成失败 {brief.get('topic', '')}: {e}", @@ -473,7 +631,7 @@ class PinterestPipeline: return None def _process_spu(self, brief: Dict[str, Any], spu, skus: str, img_code: str, - design_path: str) -> Optional[Dict[str, Any]]: + design_path: str, task_idx: int = 0) -> Optional[Dict[str, Any]]: from graph.nodes.product_node import _process_spu as _ps prod_dir = self.output_dir / "product" prod_dir.mkdir(parents=True, exist_ok=True) @@ -483,7 +641,9 @@ class PinterestPipeline: r = _ps(self._db_path, self._basemap_root, self._material_root, self._category, prod_dir, brief, self._ib, spu, skus, self.config.get("product") or {}, self._errors, design_path, self._title_backend, self.country, - img_code=img_code, model_img=self._model_assign.get(spu.get("code", ""))) + img_code=img_code, + model_img=self._model_assign.get(f"task_{task_idx}"), + on_503=lambda: (self.record_503(), self.abort_unfinished())) if r: r["img_code"] = img_code return r @@ -559,9 +719,17 @@ class PinterestPipeline: if not base or not Path(base).exists(): print(f"[pinterest_pipeline] {r.get('spu_code', '')} 参考图缺失,跳过该色种草图") continue - generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir), - r.get("composite_negative", ""), - size=size, prefix=pfx, gender=self._gender) + try: + generated = generate_seed_shots(self._ib, base, cn, material, n, str(shot_dir), + r.get("composite_negative", ""), + size=size, prefix=pfx, gender=self._gender) + except Exception as e: # noqa: BLE001 + if self.is_fatal_503(e): + self.record_503() + self.abort_unfinished() + return + print(f"[pinterest_pipeline] 种草图生成失败(跳过该色): {e}") + continue paths.extend(generated) if not paths: return diff --git a/graph/product.py b/graph/product.py index 0b25677..0f0540a 100644 --- a/graph/product.py +++ b/graph/product.py @@ -11,6 +11,8 @@ import sqlite3 from pathlib import Path from typing import Any, Dict, List, Optional +from PIL import Image + # 支持的图片格式:模特图/底图均按此识别(png/jpg 等常见格式全覆盖;AVIF/GIF/TIFF 亦支持) IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".avif", ".gif", ".tiff", ".tif"} @@ -114,6 +116,104 @@ def find_first_model_folder(material_root, preferred: Optional[str] = None): return None, [] +def image_ratio_ok(path, target_ratio: float = 3 / 4, tolerance: float = 0.06) -> bool: + """图片宽高比是否接近目标比例(默认 3:4,相对容差 6%)。 + + 相对容差:接受 [target*(1-tol), target*(1+tol)],对 3:4 即 0.705~0.795。 + 无法解析的图片(损坏/非标准)按不通过处理,避免坏图被当模特。 + """ + try: + with Image.open(path) as im: + w, h = im.size + if w <= 0 or h <= 0: + return False + ratio = w / h + lo = target_ratio * (1 - tolerance) + hi = target_ratio * (1 + tolerance) + return lo <= ratio <= hi + except Exception: # noqa: BLE001 + return False + + +def build_mark_model_map(db_path, material_root) -> Dict[str, str]: + """启动任务前检测 spu.mark 字段,建立 {mark: 模特文件夹名} 字典。 + + 规则: + - 读取 spu 表全部 mark 值(去重); + - 每个 mark 映射到 material_library/ 目录(目录名与 mark 一致); + - 目录不存在时回退到 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/)→ 用该目录; + - 否则回退 category(如 T-shirt); + - 过滤掉非 3:4 比例(默认容差 6%)的图片; + - 返回过滤后的图片列表(供调用方随机抽取)。 + """ + root = Path(material_root) + if not root.exists(): + return [] + d = None + if mark is not None: + cand = root / str(mark) + if cand.is_dir(): + d = cand + if d is None: + cand = root / category + if cand.is_dir(): + d = cand + if d is None: + # 兜底:第一个有图的目录(跳过无图目录) + for sub in sorted(root.iterdir()): + if not sub.is_dir(): + continue + if any(f.is_file() and f.suffix.lower() in IMG_EXTS for f in sub.iterdir()): + d = sub + break + if d is None: + return [] + imgs = [f for f in sorted(d.iterdir()) if f.is_file() and f.suffix.lower() in IMG_EXTS] + ok = [f for f in imgs if image_ratio_ok(f, ratio, tolerance)] + if len(ok) < len(imgs): + print(f"[product] 模特目录 {d.name}/ 过滤非 {int(ratio * 100)}:{int(ratio * 100) + 1} 比例:" + f"{len(imgs)} → {len(ok)} 张") + return ok + + def first_available_sku(db_path, basemap_root, spu_code: str) -> Optional[str]: """返回该款号下第一个「本地有底图」的 SKU.code;无则 None。""" for c in list_colors(db_path, spu_code): diff --git a/graph/state.py b/graph/state.py index 7400f24..dbd7c6c 100644 --- a/graph/state.py +++ b/graph/state.py @@ -33,6 +33,7 @@ class AgentState(TypedDict, total=False): pinterest_search_terms: List[str] # pinterest_search 产出:LLM 生成的搜索词 pinterest_images: Dict[str, List[str]] # pinterest_scrape 产出:搜索词 → 爬取图片路径列表 pinterest_briefs: List[Dict[str, Any]] # pinterest_analyze 产出:LLM 分析图片的原始设计简报 + pinterest_login: Dict[str, Any] # pinterest_scrape 产出:登录态检测结果 {status, detail, ...} # —— Pinterest 按需搜索循环状态 —— pinterest_target: int # 目标简报数(= spu_tasks 数量,每款一个设计) diff --git a/graph/template_export.py b/graph/template_export.py index a24990d..11dd11f 100644 --- a/graph/template_export.py +++ b/graph/template_export.py @@ -25,6 +25,34 @@ _COUNTRY_NAME_MAP = { "沙特": "沙特阿拉伯", } +# 成分值字典映射:db 成分值 → 女装模板下拉框选项(男装模板选项与 db 值一致,直接保留)。 +# 女装模板(如 SatVoy 沙特)成分下拉框是「中文+英文」格式(棉Cotton),db 存中文(棉),需映射。 +_COMPONENT_FEMALE_MAP = { + "棉": "棉Cotton", + "聚酯纤维": "聚酯纤维(涤纶)Polyester", + "氨纶": "氨纶Elastane", + "锦纶": "锦纶(尼龙)Polyamide", + "再生聚酯纤维": "聚酯纤维(涤纶)Polyester", # 女装无再生聚酯纤维,归入聚酯纤维 + "棉Cotton": "棉Cotton", # 已是女装格式,保持 +} + + +def _map_component(value: Any, gender: Optional[str]) -> Any: + """成分值按性别映射:male/None 保留原值;female 查女装字典(找不到保留原值)。""" + if gender != "female" or value in (None, ""): + return value + return _COMPONENT_FEMALE_MAP.get(value, value) + + +def _template_gender(template_path: str) -> Optional[str]: + """通过表头解析模版「类目」判断男装/女装:类目含「男」→male;含「女」→female;都不含→None。 + 用于成分值映射(女装模板下拉框是「中文+英文」格式,需字典映射;男装保留 db 值)。""" + try: + from graph.seed_shot import gender_from_category, read_template_category + return gender_from_category(read_template_category(template_path)) + except Exception: # noqa: BLE001 + return None + def _size_rank(size) -> tuple: """把尺码字符串转成可排序 rank(从小到大)。 @@ -206,9 +234,11 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_ def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str, color: Optional[str] = None, - fabric_headers: Optional[List[str]] = None) -> Dict[str, Any]: + fabric_headers: Optional[List[str]] = None, + gender: Optional[str] = None) -> Dict[str, Any]: """构造一行 SPU(固定字段:SKC货号=code、风格=休闲、商品产地=经营站点;多颜色时用色值列区分)。 - fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。""" + fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。 + component_1/2/3 按性别映射(gender=female 时查 _COMPONENT_FEMALE_MAP,男装/None 保留原值)。""" row: Dict[str, Any] = { "基础信息-商品层级": "spu", "SKC货号": spu_code, # code 路由为 SKC货号(用户要求) @@ -220,6 +250,8 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str, row["色值(主规格)"] = color for dbk, header in SPU_MAP.items(): v = spu.get(dbk) + if dbk in ("component_1", "component_2", "component_3"): + v = _map_component(v, gender) if v not in (None, ""): row[header] = v fabric = spu.get("fabric") @@ -251,15 +283,39 @@ def _find_sa_size_headers(router) -> List[str]: return [str(k) for k in router.column_map if "沙特阿拉伯码" in str(k)] +def _find_suggested_price_headers(router) -> List[str]: + """定位「建议售价」列(不含「单位」);匹配到多个时全部返回。""" + return [str(k) for k in router.column_map if "建议售价" in str(k) and "单位" not in str(k)] + + +def _find_suggested_unit_headers(router) -> List[str]: + """定位「建议售价单位」列。""" + return [str(k) for k in router.column_map if "建议售价单位" in str(k)] + + +def _read_suggested_required(router, col: int) -> bool: + """读取「建议售价」列下方注意事项,判断是否必填。 + 结合「非必填/必填」判断(不能只看「必填」两字):含「非必填」→非必填;否则含「必填」→必填。""" + note = str(router.ws.cell(router.header_row + 1, col).value or "") + if "非必填" in note: + return False + return "必填" in note + + def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color: str, warehouses: List[str], markup_percent: float = 0.0, multi: bool = True, price_header: str = "申报价格-日本站", bust_headers: Optional[List[str]] = None, price_headers: Optional[List[str]] = None, - sa_size_headers: Optional[List[str]] = None) -> Dict[str, Any]: + sa_size_headers: Optional[List[str]] = None, + suggested_price_ratio: float = 0.0, + suggested_price_headers: Optional[List[str]] = None, + suggested_required: Optional[Dict[str, bool]] = None, + suggested_unit_headers: Optional[List[str]] = None) -> Dict[str, Any]: """构造一行 SKU(固定字段:SPU货号、SKC货号=sku.code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。 价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100), - 预先填好。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。 + 预先填好。建议售价(suggested_price_headers 列,模板「建议售价」必填时才填)= 申报价格 × (1+suggested_price_ratio/100), + 填了建议售价同时填「建议售价单位」=CNY。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。 size 填「尺码」列 + 所有「沙特阿拉伯码」列(sa_size_headers,如 尺码表-沙特阿拉伯码,检测到才填)。 规格类型2 统一填「尺码」两个字(不是 size 参数值)。""" row: Dict[str, Any] = { @@ -295,6 +351,14 @@ def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color: v = round(float(v) * (1 + markup_percent / 100), 2) # 申报价格 = price × (1+加价%) for h in price_headers: row[h] = v + # 建议售价 = 申报价格 × (1+建议售价比例%);模板「建议售价」必填时才填,填了同时填单位 CNY + if suggested_price_headers and suggested_price_ratio > 0: + suggested = round(v * (1 + suggested_price_ratio / 100), 2) + for h in suggested_price_headers: + if (suggested_required or {}).get(h, True): + row[h] = suggested + for uh in suggested_unit_headers: + row[uh] = "CNY" continue if dbk == "size": if v in (None, ""): @@ -445,6 +509,11 @@ def _insert_product_block( bust_headers: Optional[List[str]] = None, fabric_headers: Optional[List[str]] = None, sa_size_headers: Optional[List[str]] = None, + gender: Optional[str] = None, + suggested_price_ratio: float = 0.0, + suggested_price_headers: Optional[List[str]] = None, + suggested_required: Optional[Dict[str, bool]] = None, + suggested_unit_headers: Optional[List[str]] = None, ) -> List[int]: """在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。 @@ -474,7 +543,8 @@ def _insert_product_block( # 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充) # + 全部颜色尺码 SKU 行(色值在 SKU 行区分) block_rows.append(router.insert( - _build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers), + _build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers, + gender=gender), match="exact", )) for ci, (sc, skus) in enumerate(skus_by_color): @@ -488,7 +558,11 @@ def _insert_product_block( _build_sku_row(spu_code, sc, sk, size, color, warehouses, markup_percent=markup_percent, multi=True, bust_headers=bust_headers, price_headers=price_headers, - sa_size_headers=sa_size_headers), + sa_size_headers=sa_size_headers, + suggested_price_ratio=suggested_price_ratio, + suggested_price_headers=suggested_price_headers, + suggested_required=suggested_required, + suggested_unit_headers=suggested_unit_headers), spu_code=spu_code, match="exact", )) @@ -504,7 +578,8 @@ def _insert_product_block( else: # 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分) block_rows.append(router.insert( - _build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers), match="exact")) + _build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers, + gender=gender), match="exact")) multi_variant = len(skus_by_color) > 1 for ci, (sc, skus) in enumerate(skus_by_color): first = skus[0] @@ -515,7 +590,11 @@ def _insert_product_block( _build_sku_row(spu_code, sc, sk, size, color, warehouses, markup_percent=markup_percent, multi=multi_variant, bust_headers=bust_headers, price_headers=price_headers, - sa_size_headers=sa_size_headers), + sa_size_headers=sa_size_headers, + suggested_price_ratio=suggested_price_ratio, + suggested_price_headers=suggested_price_headers, + suggested_required=suggested_required, + suggested_unit_headers=suggested_unit_headers), )) sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else [] _fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs) @@ -554,6 +633,7 @@ def export_product( seed_shot_urls: Optional[List[str]] = None, append_to: str = "", markup_percent: float = 0.0, + suggested_price_ratio: float = 0.0, ) -> Path: """生成商品上传"已填写"模板(支持多产品合并到同一文件)。 @@ -584,6 +664,11 @@ def export_product( bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填) fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填) sa_size_headers = _find_sa_size_headers(router) # 沙特阿拉伯码列(尺码表-沙特阿拉伯码…检测到才填) + gender = _template_gender(template_path) # 类目含「男」→male /「女」→female(成分值映射用) + suggested_price_headers = _find_suggested_price_headers(router) # 建议售价列(不含单位) + suggested_unit_headers = _find_suggested_unit_headers(router) # 建议售价单位列 + suggested_required = {h: _read_suggested_required(router, router.column_map[h]) + for h in suggested_price_headers} # 各建议售价列是否必填(结合非必填/必填判断) _insert_product_block(router, db_path, spu_code, sku_code, origin_province, warehouses, price_headers, markup_percent=markup_percent, images=images, @@ -594,7 +679,12 @@ def export_product( seed_shot_urls=seed_shot_urls, bust_headers=bust_headers, fabric_headers=fabric_headers, - sa_size_headers=sa_size_headers) + sa_size_headers=sa_size_headers, + gender=gender, + suggested_price_ratio=suggested_price_ratio, + suggested_price_headers=suggested_price_headers, + suggested_required=suggested_required, + suggested_unit_headers=suggested_unit_headers) out = router.save(out_path) return Path(out) finally: @@ -611,6 +701,7 @@ def export_products( template_path: str, out_path: str, markup_percent: float = 0.0, + suggested_price_ratio: float = 0.0, ) -> Path: """批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。 @@ -628,6 +719,11 @@ def export_products( bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填) fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填) sa_size_headers = _find_sa_size_headers(router) # 沙特阿拉伯码列(尺码表-沙特阿拉伯码…检测到才填) + gender = _template_gender(template_path) # 类目含「男」→male /「女」→female(成分值映射用) + suggested_price_headers = _find_suggested_price_headers(router) # 建议售价列(不含单位) + suggested_unit_headers = _find_suggested_unit_headers(router) # 建议售价单位列 + suggested_required = {h: _read_suggested_required(router, router.column_map[h]) + for h in suggested_price_headers} # 各建议售价列是否必填(结合非必填/必填判断) for r in products: try: _insert_product_block( @@ -647,6 +743,11 @@ def export_products( bust_headers=bust_headers, fabric_headers=fabric_headers, sa_size_headers=sa_size_headers, + gender=gender, + suggested_price_ratio=suggested_price_ratio, + suggested_price_headers=suggested_price_headers, + suggested_required=suggested_required, + suggested_unit_headers=suggested_unit_headers, ) except Exception as e: # noqa: BLE001 print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}") diff --git a/graph/validate.py b/graph/validate.py index 6514ace..1efa801 100644 --- a/graph/validate.py +++ b/graph/validate.py @@ -8,10 +8,34 @@ 剔除非法记录并记录原因,保证下游拿到的数据"形状正确"。 """ import functools +import threading import traceback from typing import Any, Dict, List +class ThreadSafeErrors: + """线程安全的错误收集器:并发节点(compose/product 等)内 append 错误用。 + + 避免多个 worker 线程直接写共享 list 造成竞态;主线程统一合并到 state['errors']。 + """ + + def __init__(self) -> None: + self._lock = threading.Lock() + self._items: List[Dict[str, Any]] = [] + + def append(self, item: Dict[str, Any]) -> None: + with self._lock: + self._items.append(item) + + def __iter__(self): + with self._lock: + return iter(list(self._items)) + + def __len__(self) -> int: + with self._lock: + return len(self._items) + + def with_fallback(node_name: str): """装饰器:捕获节点异常,转为 state['errors'] 中的一条记录,返回空更新。 diff --git a/pinterest_scraper/pinterest_image_capture.py b/pinterest_scraper/pinterest_image_capture.py index 6f070c8..c78312f 100644 --- a/pinterest_scraper/pinterest_image_capture.py +++ b/pinterest_scraper/pinterest_image_capture.py @@ -5,7 +5,9 @@ import os import random import socket import sys +import time import urllib.request +from typing import Any, Dict from urllib.parse import urlparse, quote import aiohttp @@ -218,6 +220,80 @@ def _session_user_data_dir() -> str: return ud +def check_login_state() -> Dict[str, Any]: + """静态检测 Pinterest 登录态(不启动 Chrome,只读持久化 cookies 数据库)。 + + 通过检查 .chrome_session/User Data 里的 Chrome cookies 数据库: + - 无目录 / 无 cookies 文件 → no_session(从未登录过) + - 有 cookies 但无 Pinterest 域 cookie → logged_out(未登录过 Pinterest) + - 有 Pinterest 域 cookie 但缺关键登录 cookie(_auth/csrftoken)或已过期 → logged_out + - 含 _auth 且未过期 → logged_in + - 数据库无法解析 → unknown(调用方走运行时检测兜底) + + 注意:Chrome 在 Windows 上对 cookie 值做 DPAPI 加密,这里只判断 + cookie 的存在性 / 域名 / 过期时间,不解析密文值。 + """ + ud = _session_user_data_dir() + out: Dict[str, Any] = { + "status": "unknown", + "session_dir": ud, + "cookies_file": "", + "pinterest_cookies": 0, + "has_auth_cookie": False, + "expired": False, + "detail": "", + } + if not os.path.isdir(ud): + out["status"] = "no_session" + out["detail"] = "未找到 Chrome 登录态目录(从未登录过 Pinterest)" + return out + cookies_file = None + for rel in ("Default/Network/Cookies", "Default/Cookies"): + cand = os.path.join(ud, rel) + if os.path.isfile(cand) and os.path.getsize(cand) > 0: + cookies_file = cand + break + if not cookies_file: + out["status"] = "no_session" + out["detail"] = "未找到 cookies 数据库(从未登录过 Pinterest)" + return out + out["cookies_file"] = cookies_file + try: + import sqlite3 + conn = sqlite3.connect(f"file:{cookies_file}?mode=ro&immutable=1", uri=True) + try: + rows = conn.execute( + "SELECT host_key, name, expires_utc FROM cookies " + "WHERE host_key LIKE '%pinterest.com%' OR host_key LIKE '%pinimg.com%'" + ).fetchall() + finally: + conn.close() + except Exception as e: # noqa: BLE001 + out["detail"] = f"cookies 读取失败: {e}" + return out + out["pinterest_cookies"] = len(rows) + # WebKit 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, sem: asyncio.Semaphore, save_dir: str, proxy: str | None): filename = get_filename_from_url(url, idx) @@ -284,7 +360,7 @@ async def human_scroll(page: Page, distance: int): await page.wait_for_timeout(random.randint(30, 100)) # 滚轮事件间隔 -async def _ensure_logged_in(page: Page, search_url: str) -> bool: +async def _ensure_logged_in(page: Page, search_url: str, login_wait: bool = False) -> bool: """独立登录检查方法:在已跳到搜索页的前提下确认 Pinterest 已登录。 判定策略(避免误判): @@ -292,7 +368,9 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool: 才认定未登录;否则默认已登录(不依赖可能不匹配的已登录选择器)。 - Pinterest 是 SPA,goto 后需等待渲染,否则瞬间误判未登录。 - 已登录 → 立即返回 True,走直路。 - - 未登录(被弹回登录墙)→ 回退首页提示手动登录一次,登录成功后返回 True。 + - 未登录: + login_wait=True → 回退首页提示手动登录一次,登录成功后返回 True; + login_wait=False → 不阻塞,直接返回 False(由调用方跳过并告警)。 - 一直未登录(用户关窗口/放弃)→ 返回 False。 """ async def _has_login_wall() -> bool: @@ -327,6 +405,13 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool: print("✅ 已检测到登录态,直接开始搜索") return True + # 未登录:默认不阻塞(跳过并告警);login_wait=True 才回退首页等手动登录 + if not login_wait: + print("⚠️ 未检测到 Pinterest 登录态(cookie 缺失或已过期)。" + "请先在 pinterest_scraper/.chrome_session 目录登录 Pinterest 一次," + "或设置 pinterest.login_wait=true 让程序自动等待手动登录。") + return False + # 未登录(搜索页被弹回登录墙):回退首页,提示手动登录一次,成功后立即继续 print("⚠️ 当前未登录(搜索页被拦截)。请在弹出的浏览器窗口中手动登录,登录成功后将自动继续……") try: @@ -351,7 +436,8 @@ async def _ensure_logged_in(page: Page, search_url: str) -> bool: async def scrape(keyword: str, count: int, headless: bool = False, proxy: str | None = None, - cdp_url: str | None = None) -> set[str]: + cdp_url: str | None = None, + login_wait: bool = False) -> set[str]: imgs_url: set[str] = set() async with async_playwright() as p: if cdp_url: @@ -389,7 +475,7 @@ async def scrape(keyword: str, count: int, headless: bool = False, await page.goto(search_url, wait_until="domcontentloaded") # 独立登录检查:已登录直接开始;未登录才回退首页等手动登录 - logged_in = await _ensure_logged_in(page, search_url) + logged_in = await _ensure_logged_in(page, search_url, login_wait=login_wait) if not logged_in: # 关闭浏览器(持久化目录已保存任何已有状态),中止本次爬取 if cdp_url: diff --git a/pinterest_scraper/scraper.py b/pinterest_scraper/scraper.py index 62e3cbd..27ba6fb 100644 --- a/pinterest_scraper/scraper.py +++ b/pinterest_scraper/scraper.py @@ -19,11 +19,13 @@ from pinterest_scraper.pinterest_image_capture import ( def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = None, proxy: Optional[str] = None, headless: bool = False, - cdp_url: Optional[str] = None) -> List[str]: + cdp_url: Optional[str] = None, + login_wait: bool = False) -> List[str]: """按关键词爬取 Pinterest 图片并下载到 save_dir,返回下载成功的文件路径列表。 - proxy 为 None 时自动探测(环境变量/系统代理/本地常见端口);空字符串显式禁用。 - - 首次运行会弹出本地 Chrome 窗口,需手动登录 Pinterest 一次,登录态缓存在 + - login_wait=False(默认):运行时检测到未登录时不阻塞,直接返回空列表(由调用方跳过并告警); + login_wait=True:弹出 Chrome 窗口等待手动登录一次,登录态缓存在 pinterest_scraper/.chrome_session,之后自动复用。 """ # 代理解析:显式传入 > 自动探测 > 直连 @@ -39,7 +41,7 @@ def scrape_pinterest(keyword: str, count: int = 40, save_dir: Optional[str] = No os.makedirs(out, exist_ok=True) print(f"[pinterest] 关键词: {keyword} | 数量: {count} | 保存目录: {out}") - imgs_url = asyncio.run(scrape(keyword, count, headless, proxy, cdp_url)) + imgs_url = asyncio.run(scrape(keyword, count, headless, proxy, cdp_url, login_wait)) if not imgs_url: print(f"[pinterest] 未收集到图片(可能未登录或搜索无结果): {keyword}") return [] diff --git a/templates/template_router.py b/templates/template_router.py index 2985a0d..47a866a 100644 --- a/templates/template_router.py +++ b/templates/template_router.py @@ -406,7 +406,18 @@ class TemplateRouter: # 2) 路由到目标行 row = self.route(lv, spu, update=update, match=match) - # 3) 写入 + # 3) 清空目标行(仅新建行,update 覆盖模式保留模板原有内容): + # 模板数据区可能预填了示例/默认值(如 SKU销售信息-建议售价单位=CNY、 + # SPU商品属性-印花类型=定位印花),不清理会把模板预填值误带到生成结果里。 + # 只清空本行已写过的数据单元格,保留样式/数据验证下拉框。 + if not update: + for c in range(1, self.ws.max_column + 1): + cell = self.ws.cell(row, c) + v = cell.value + if v is not None and str(v).strip() != "": + cell.value = None + + # 4) 写入 for key, value in data.items(): if value is None: continue diff --git a/对接文档.md b/对接文档.md deleted file mode 100644 index 15566a3..0000000 --- a/对接文档.md +++ /dev/null @@ -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/.yaml# ★每国独立配置:种子词/related 种子/采集限额 -├── prompts// # ★每国提示词: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/.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/.yaml`(种子词)+ `prompts//`(提示词)+ `holidays.py`(节日)+ `config.yaml countries 列表` + UI 国家下拉 + 模板站点映射 → mock 跑通 → 真 LLM 验证。