模板导出增强 + 模特性别分组 + 三合一提示词精简
1) 模板导出:识别「基码表-胸围」填 sku.bust(多个胸围列都填);申报价格模糊匹配多列统一按加价后价格填写;详情图文不再拼接 img_url_2;SPU 款式来源统一填「现货款」;商品产地国家简称映射(沙特→沙特阿拉伯) 2) 模特性别分组:model_features 按男女分组,按模板类目含男/女固定取对应性别模特(含 Pinterest 模式 pipeline) 3) 三合一提示词:去掉 DESIGN CONTENT 四要素描述(设计已由设计稿提供) 4) 生图尺寸:全部改为读 config 不再硬编码(设计图 compose.design_size / 合成图 compose.size / 种草图 seed_shot.size)
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 578 KiB |
|
After Width: | Height: | Size: 970 KiB |
|
After Width: | Height: | Size: 690 KiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 799 KiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 928 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 499 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
@@ -34,18 +34,25 @@ seed_provider_cfg:
|
|||||||
history_limit: 20
|
history_limit: 20
|
||||||
|
|
||||||
# —— Pinterest 参考模式(独立于 Google Trends 采集,UI「Pinterest 参考模式」入口)——
|
# —— Pinterest 参考模式(独立于 Google Trends 采集,UI「Pinterest 参考模式」入口)——
|
||||||
# 流程:各国独立种子词池(configs/pinterest/<CC>.yaml) → LLM 生成搜索词(json_schema+防重复)
|
# 流程:各国独立种子词池(configs/pinterest/<CC>.yaml) → LLM 按需生成搜索词(json_schema+防重复)
|
||||||
# → 过滤 → scraper 爬取图片 → LLM 分析图片 → 构造提示词 → 生成设计
|
# → 爬图 → LLM 分析图片 → 构造提示词 → 生成设计
|
||||||
|
# 按需搜索:每次只生成 1 个搜索词,用完(爬取成功)才标记已用;简报不足时循环再搜,直到满足 SPU 数量。
|
||||||
pinterest:
|
pinterest:
|
||||||
enabled: true
|
enabled: true
|
||||||
provider: openai # 搜索词/图片分析用 LLM 提供商(openai / mock)
|
provider: openai # 搜索词/图片分析用 LLM 提供商(openai / mock)
|
||||||
search_terms_per_run: 10 # 每次运行 LLM 生成的搜索词数量
|
search_mode: direct # 搜索词生成方式:direct=跳过LLM,直接搜「种子词 t-shirt design」;llm=LLM按需生成搜索词
|
||||||
|
search_terms_per_run: 1 # 每次搜索词数量(direct 模式=从种子池随机取 N 个直接拼后缀;llm 模式=每次生成 1 个)
|
||||||
|
max_search_rounds: 0 # 搜索轮次上限(0=自动:目标 SPU 数×2,至少 5;防网络故障无限循环)
|
||||||
seed_sample: 40 # 每次从国家种子池随机抽取多少个种子词给 LLM
|
seed_sample: 40 # 每次从国家种子池随机抽取多少个种子词给 LLM
|
||||||
max_used_terms_in_prompt: 100 # 已用搜索词最多注入 LLM 提示词的个数(防 token 超限)
|
max_used_terms_in_prompt: 100 # 已用搜索词最多注入 LLM 提示词的个数(防 token 超限)
|
||||||
images_per_term: 40 # 每个搜索词爬取图片数量
|
images_per_term: 40 # 每个搜索词爬取图片数量
|
||||||
analyze_per_term: 6 # 每个搜索词最多分析几张图(生成设计简报)
|
analyze_per_term: 1 # 每次调用分析 1 张图(一张对应一个设计,一次 API 请求;简报带 image_index 全局 id 校验防错位)
|
||||||
max_designs: 10 # 本次最多生成多少个设计
|
analyze_concurrency: 6 # 并发分析线程数(多并发提取简报)
|
||||||
|
analyze_batch: 0 # 每轮从图池取多少张图分析(0=自动:一次补齐到目标所需/每词简报上限,让 pipeline 队列时刻满并发)
|
||||||
|
max_designs: 10 # 每个搜索词最多生成多少个设计简报
|
||||||
ref_images_per_design: 1 # 生图时每个设计附带几张爬取图作为参考(发给生图模型)
|
ref_images_per_design: 1 # 生图时每个设计附带几张爬取图作为参考(发给生图模型)
|
||||||
|
supply_attempts: 3 # 设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数
|
||||||
|
err400_limit: 15 # 每个种子词累计 400(且错误含「内容/图片」)超限后放弃该种子词(多模态+生图合计)
|
||||||
scrape_concurrency: 1 # 同时爬取几个搜索词(共享 .chrome_session 登录态,Chrome 单例,必须=1)
|
scrape_concurrency: 1 # 同时爬取几个搜索词(共享 .chrome_session 登录态,Chrome 单例,必须=1)
|
||||||
headless: false # 爬取时是否无头(false=显示 Chrome 窗口,首次需手动登录)
|
headless: false # 爬取时是否无头(false=显示 Chrome 窗口,首次需手动登录)
|
||||||
|
|
||||||
@@ -98,20 +105,21 @@ query_noise:
|
|||||||
llm_screen:
|
llm_screen:
|
||||||
enabled: true
|
enabled: true
|
||||||
provider: openai # openai(真 LLM,需 key)/ mock(无 key 启发式兜底)
|
provider: openai # openai(真 LLM,需 key)/ mock(无 key 启发式兜底)
|
||||||
api_key: "sk-ws-H.EPXPIER.vRMT.MEUCIQDji_AHGl-EekYOftdLxEvFl2ZqCNtLDSp3Bqnaz0SRpgIgI4qPeVyIMMzutl9JakuYRv3zmFxkwc12c4aBcIL1gqE" # 留空则自动读取环境变量 LLM_API_KEY / OPENAI_API_KEY(推荐,避免密钥入库)
|
api_key: "ark-607f7194-f253-4c55-a421-237317d7683e-2bf08" # 留空则自动读取环境变量 LLM_API_KEY / OPENAI_API_KEY(推荐,避免密钥入库)
|
||||||
base_url: "https://ws-5rfjflubus647o7t.cn-beijing.maas.aliyuncs.com/compatible-mode/v1" # 自定义网关地址,也可用环境变量 LLM_BASE_URL 覆盖
|
base_url: "https://ark.cn-beijing.volces.com/api/v3" # 自定义网关地址,也可用环境变量 LLM_BASE_URL 覆盖
|
||||||
model: "qwen3.7-plus-2026-05-26" # 自定义模型名(任意 OpenAI 兼容模型)
|
model: "doubao-seed-2-0-lite-260215" # 自定义模型名(任意 OpenAI 兼容模型)
|
||||||
temperature: 0.6
|
temperature: 0.6
|
||||||
max_topics_per_call: 12
|
max_topics_per_call: 12
|
||||||
min_score: 0.0
|
min_score: 0.0
|
||||||
keep_review: false # false = 待复核(review)直接过滤、不生成设计提示词,只留 safe
|
keep_review: false # false = 待复核(review)直接过滤、不生成设计提示词,只留 safe
|
||||||
|
max_briefs: 0 # 简报数量上限(0=自动:按扩展后总任务数/SPU数,用多少生成多少)
|
||||||
|
|
||||||
# —— 固定提示词模板(规则写死,保证每条一致)——
|
# —— 固定提示词模板(规则写死,保证每条一致)——
|
||||||
# 由 motif + art_style + color_palette + composition 四要素按模板确定性拼出。
|
# 由 motif + art_style + color_palette + composition 四要素按模板确定性拼出。
|
||||||
# v3:countries.<CODE> 按国家覆盖 image_prompt(每国设计风格不同),顶层为兜底。
|
# v3:countries.<CODE> 按国家覆盖 image_prompt(每国设计风格不同),顶层为兜底。
|
||||||
# 文字规则统一:英文可加可不加、适配印花即可;任何文字严禁政治/宗教/仇恨/暴力/性/品牌/商标/真实人物等敏感内容。
|
# 文字规则统一:英文可加可不加、适配印花即可;任何文字严禁政治/宗教/仇恨/暴力/性/品牌/商标/真实人物等敏感内容。
|
||||||
prompt_templates:
|
prompt_templates:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, US-market aesthetic: bold confident statement graphic, high contrast, clean modern vector, sporty or humorous mood, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, US-market aesthetic: bold confident statement graphic, high contrast, clean modern vector, sporty or humorous mood, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
wearable_prompt: "{motif}, {art_style}, {color_palette}, {composition}, printed centered on the chest of a flat-lay plain white t-shirt, print sized freely between about 15x18 cm and a max of 26x32 cm, scaled naturally to the artwork, not stretched, not full-bleed, studio lighting, e-commerce product photo, no human model"
|
wearable_prompt: "{motif}, {art_style}, {color_palette}, {composition}, printed centered on the chest of a flat-lay plain white t-shirt, print sized freely between about 15x18 cm and a max of 26x32 cm, scaled naturally to the artwork, not stretched, not full-bleed, studio lighting, e-commerce product photo, no human model"
|
||||||
composite_prompt: "【图片角色,按提交顺序】图1=模特实拍图(基底);图2=纯印花设计稿;图3=平铺衣服底图(只取衣服本身的底色与面料材质,忽略平铺图背景/桌面/场景,只保留面料颜色与质感)。
|
composite_prompt: "【图片角色,按提交顺序】图1=模特实拍图(基底);图2=纯印花设计稿;图3=平铺衣服底图(只取衣服本身的底色与面料材质,忽略平铺图背景/桌面/场景,只保留面料颜色与质感)。
|
||||||
TASK: 把图2的印花设计印到图3底色的衣服上,并让图1的模特穿上“图3底色+图2印花”的衣服。
|
TASK: 把图2的印花设计印到图3底色的衣服上,并让图1的模特穿上“图3底色+图2印花”的衣服。
|
||||||
@@ -120,34 +128,33 @@ RULES:/n1.底色锁定:从图3提取衣服底色与面料,最终合成中必须1
|
|||||||
3.主体遮罩:识别图1模特服装穿着区域(忽略皮肤/头发/背景/配饰),用合成面料完整覆盖,清除原衣服颜色与图案。
|
3.主体遮罩:识别图1模特服装穿着区域(忽略皮肤/头发/背景/配饰),用合成面料完整覆盖,清除原衣服颜色与图案。
|
||||||
4.精准贴合:合成面料严格跟随图1衣服立体结构,褶皱/扭转处印花相应变形,杜绝“贴纸感”与“平面涂色感”。
|
4.精准贴合:合成面料严格跟随图1衣服立体结构,褶皱/扭转处印花相应变形,杜绝“贴纸感”与“平面涂色感”。
|
||||||
5.光影融合:按图1环境光方向调整亮度/对比度,印花受光影响产生明暗变化但色号不偏移。
|
5.光影融合:按图1环境光方向调整亮度/对比度,印花受光影响产生明暗变化但色号不偏移。
|
||||||
6.纯净输出:仅输出一张最终合成图;图1背景/人物/构图/光影100%不变,仅替换衣服印花与底色。
|
6.纯净输出:仅输出一张最终合成图;图1背景/人物/构图/光影100%不变,仅替换衣服印花与底色。"
|
||||||
DESIGN CONTENT: {motif}, {art_style}, {color_palette}, {composition}."
|
|
||||||
composite_negative: "garment changed, wrong color, distorted print, blurry, low-res, human model, body, extra objects, watermark, glow, 3d render, text unless part of design"
|
composite_negative: "garment changed, wrong color, distorted print, blurry, low-res, human model, body, extra objects, watermark, glow, 3d render, text unless part of design"
|
||||||
countries:
|
countries:
|
||||||
US:
|
US:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, US-market aesthetic: bold confident statement graphic, high contrast, clean modern vector, sporty or humorous mood, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, US-market aesthetic: bold confident statement graphic, high contrast, clean modern vector, sporty or humorous mood, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
GB:
|
GB:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, UK-market aesthetic: witty understated British charm, heritage-inspired motifs, retro sportswear or punk-zine mood, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, UK-market aesthetic: witty understated British charm, heritage-inspired motifs, retro sportswear or punk-zine mood, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
JP:
|
JP:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, JP-market aesthetic: kawaii cute or clean minimal, soft pastel-friendly, polished neat lines, small cute mascot mood, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, JP-market aesthetic: kawaii cute or clean minimal, soft pastel-friendly, polished neat lines, small cute mascot mood, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
AU:
|
AU:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, AU-market aesthetic: laid-back coastal and outdoor vibe, nature-inspired, bright fresh energy, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, AU-market aesthetic: laid-back coastal and outdoor vibe, nature-inspired, bright fresh energy, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a small slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
MX:
|
MX:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, MX-market aesthetic: vibrant mexican folk art, sugar skull / loteria / aztec motifs, fiesta colors, festive cultural pride mood, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Spanish slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, MX-market aesthetic: vibrant mexican folk art, sugar skull / loteria / aztec motifs, fiesta colors, festive cultural pride mood, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Spanish slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
DE:
|
DE:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, DE-market aesthetic: bavarian-alpine folk charm, berlin street-art edge, bauhaus minimalism, cozy beer-garden and christmas-market mood, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a German slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, DE-market aesthetic: bavarian-alpine folk charm, berlin street-art edge, bauhaus minimalism, cozy beer-garden and christmas-market mood, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a German slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
BR:
|
BR:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, BR-market aesthetic: vibrant tropical carnival energy, samba rhythm, football passion, bold beach and street-art colors, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Portuguese slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, BR-market aesthetic: vibrant tropical carnival energy, samba rhythm, football passion, bold beach and street-art colors, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Portuguese slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
SA:
|
SA:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, SA-market aesthetic: elegant desert minimalism, arabian geometric patterns, falconry and starry-night calm, refined and conservative mood, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or an Arabic slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, SA-market aesthetic: elegant desert minimalism, arabian geometric patterns, falconry and starry-night calm, refined and conservative mood, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or an Arabic slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
PL:
|
PL:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, PL-market aesthetic: highland folk embroidery charm, wycinanki paper-cut patterns, slavic forest mystique, retro polish poster mood, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Polish slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, PL-market aesthetic: highland folk embroidery charm, wycinanki paper-cut patterns, slavic forest mystique, retro polish poster mood, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Polish slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
ES:
|
ES:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, ES-market aesthetic: flamenco passion, andalusian tile patterns, mediterranean sunshine, festive fiesta energy, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Spanish slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, ES-market aesthetic: flamenco passion, andalusian tile patterns, mediterranean sunshine, festive fiesta energy, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a Spanish slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
IT:
|
IT:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, IT-market aesthetic: renaissance elegance, roman heritage, tuscan countryside warmth, la dolce vita retro charm, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or an Italian slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, IT-market aesthetic: renaissance elegance, roman heritage, tuscan countryside warmth, la dolce vita retro charm, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or an Italian slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
CA:
|
CA:
|
||||||
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, CA-market aesthetic: maple-leaf pride, northern-lights wonder, rocky-mountain outdoors, cozy cottage and hockey spirit, size: choose freely between a MINIMUM print area of about 15x18 cm and a MAXIMUM of 26x32 cm, any size in this range fits, pick the one that best suits the design, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave balanced margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a French slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
image_prompt: "{motif}, {art_style}, {color_palette}, {composition}, standalone pure print design, print-ready artwork, isolated on pure white background, no background, no scene, no texture, no frame, no border, flat vector-like graphic, crisp clean edges, high resolution, ultra sharp, high contrast, CA-market aesthetic: maple-leaf pride, northern-lights wonder, rocky-mountain outdoors, cozy cottage and hockey spirit, size: the print artwork must be SMALL and CENTERED with clearly larger white margins around it, print area between about 15x18 cm and 26x32 cm, keep proportions, scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, do NOT force full-bleed, leave wide balanced white margins around the artwork, no garment, no shirt, no model, no mannequin, no watermark, text: optional - add short original English words or a French slogan ONLY if they fit the print, or keep it text-free; any text must be safe, short and original; no brand names, no logos, no trademarked phrases, no real people names"
|
||||||
|
|
||||||
# —— 第三阶段 compose:生成纯印花设计稿 + 导出简报包(在 product 之前)——
|
# —— 第三阶段 compose:生成纯印花设计稿 + 导出简报包(在 product 之前)——
|
||||||
# 用前 N 个 safe 简报的 image_prompt 调图像后端文生图,产物 output/<country>/designs/,
|
# 用前 N 个 safe 简报的 image_prompt 调图像后端文生图,产物 output/<country>/designs/,
|
||||||
@@ -156,11 +163,14 @@ DESIGN CONTENT: {motif}, {art_style}, {color_palette}, {composition}."
|
|||||||
compose:
|
compose:
|
||||||
backend: "openai" # 生成设计稿的图像后端:openai(需 api_key)/ mock(占位);留空=不生成
|
backend: "openai" # 生成设计稿的图像后端:openai(需 api_key)/ mock(占位);留空=不生成
|
||||||
api_key: "sk-ef1f251f059f9c2f8cc397de4a84e96d65775e1f2d32bb13b69b23dafc24d111"
|
api_key: "sk-ef1f251f059f9c2f8cc397de4a84e96d65775e1f2d32bb13b69b23dafc24d111"
|
||||||
base_url: "https://api.tofastcode.xyz" # 自定义图像网关地址(默认 OpenAI)
|
base_url: "https://api.tofastcode.xyz" # 自定义图像网关地址(默认 OpenAI)——填 API 根路径(含 /v1),不要带 /images/edits 后缀,否则拼接路径 404
|
||||||
model: "gpt-image-2"
|
model: "gpt-image-2"
|
||||||
size: "1536×2048"
|
size: "1536x2048" # 合成图/种草图等非设计图的统一尺寸(3:4)
|
||||||
|
design_size: "1024x1024" # 纯印花设计稿尺寸(1:1)
|
||||||
background: "transparent" # 生成透明背景 PNG(gpt-image 系列支持);留空=默认背景
|
background: "transparent" # 生成透明背景 PNG(gpt-image 系列支持);留空=默认背景
|
||||||
|
seed: 0 # 随机种子:>0=固定(相同 seed 可复现,需网关支持);0/留空=每次随机
|
||||||
design_count: 1 # 用前 N 个 safe 简报生成设计稿(product 取第一个复用)
|
design_count: 1 # 用前 N 个 safe 简报生成设计稿(product 取第一个复用)
|
||||||
|
design_workers: 5 # 设计稿并发生成线程数(仅热点采集模式 compose 节点用;Pinterest 模式走 pipeline 总并发 product.concurrency)
|
||||||
|
|
||||||
# —— 第五阶段 oss_upload:压缩(3:4 / ≥1340×1785 / <2MB)+ 上传阿里云 OSS 图床 ——
|
# —— 第五阶段 oss_upload:压缩(3:4 / ≥1340×1785 / <2MB)+ 上传阿里云 OSS 图床 ——
|
||||||
# 产物(composite/printed/design/basemap)逐个压缩上传,URL 写回 *_url 字段。
|
# 产物(composite/printed/design/basemap)逐个压缩上传,URL 写回 *_url 字段。
|
||||||
@@ -180,6 +190,7 @@ oss:
|
|||||||
seed_shot:
|
seed_shot:
|
||||||
enabled: true
|
enabled: true
|
||||||
count: 1 # 每个产品选取几张种草图(从三合一主图中随机取,传入数量即可)
|
count: 1 # 每个产品选取几张种草图(从三合一主图中随机取,传入数量即可)
|
||||||
|
size: "1536x2048" # 种草图生成尺寸(3:4,与合成图一致)
|
||||||
|
|
||||||
# —— 第四阶段 product:热点 → SPU/颜色选品 → 底图 → 三图合成(图1模特+图2设计稿+图3底图)→ 模板 ——
|
# —— 第四阶段 product:热点 → SPU/颜色选品 → 底图 → 三图合成(图1模特+图2设计稿+图3底图)→ 模板 ——
|
||||||
# 数据关系:SPU.code=款号,SKU.code="款号-颜色编码"(如 DG004-BL01),
|
# 数据关系:SPU.code=款号,SKU.code="款号-颜色编码"(如 DG004-BL01),
|
||||||
@@ -202,4 +213,5 @@ product:
|
|||||||
background: "transparent" # 设计稿透明背景(product 后端生图时也传 background=transparent)
|
background: "transparent" # 设计稿透明背景(product 后端生图时也传 background=transparent)
|
||||||
template_dir: "templates" # template_router.py 所在目录(项目内自包含)
|
template_dir: "templates" # template_router.py 所在目录(项目内自包含)
|
||||||
template_path: "templates/商品上传模版.xlsx" # 商品上传模板(可改为自由上传)
|
template_path: "templates/商品上传模版.xlsx" # 商品上传模板(可改为自由上传)
|
||||||
|
concurrency: 5 # Pinterest 模式 pipeline 总并发(设计生成→三合一→种草图→标题 同一管道串行,总并发=此值)
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,75 @@
|
|||||||
# 模特特征库 —— 可自定义添加(一行一条,种草图提示词 [模特特征] 随机取用)
|
# 模特特征库 —— 跨境电商基础款服装展示专用(已剔除外部穿搭与夸张妆造干扰)
|
||||||
# 示例:
|
# 占位符:[模特特征] 随机取用
|
||||||
# - "20岁清新少女,素颜通透感"
|
# 注意:所有特征仅描述长相、肤质、气质,不含外套或多余服装。
|
||||||
# - "25岁都市职场女性,干练气质"
|
# 按性别分组:female=女模 / male=男模。
|
||||||
|
# 商品类目(模版「类目」表头值)含「男」→ 固定从 male 随机;含「女」→ 固定从 female 随机;都不含 → 全部随机。
|
||||||
|
|
||||||
model_features:
|
model_features:
|
||||||
- "20岁欧美甜心,金发碧眼,阳光加州感"
|
female:
|
||||||
- "25岁法国左岸文青,法式刘海,慵懒红唇"
|
# ========== 女模 - 亚洲/清新/通勤类(适合基础款、纯色T恤) ==========
|
||||||
- "28岁意大利名媛,复古波浪卷,精致上扬眼线"
|
- "22岁亚洲清新女学生,通透伪素颜,黑直发,清瘦骨感"
|
||||||
- "22岁纽约下城区酷女孩,挑染发色,厌世烟熏妆"
|
- "25岁日韩系通勤女模,元气通透底妆,棕色微卷发,亲切感"
|
||||||
- "30岁北欧极简风女性,冷白皮,无瑕光泽肌"
|
- "27岁亚洲极简风女模,冷白皮,无瑕光泽肌,利落黑色短发"
|
||||||
- "24岁拉美热情系女生,小麦色肌肤,野生挑眉"
|
- "29岁亚洲轻熟女模,骨相美,微雾感底妆,气质温婉"
|
||||||
- "26岁法国时尚博主,法式码头上衣,碎发自然感"
|
|
||||||
- "29岁英伦中性风女模,深色短寸,骨感清冷"
|
# ========== 女模 - 欧美/阳光/甜心类(适合印花T恤、度假风) ==========
|
||||||
- "21岁俄罗斯芭蕾少女,深邃眼窝,通透纯欲妆"
|
- "20岁欧美甜心女模,金发碧眼,阳光加州感,健康小麦色肌肤"
|
||||||
- "25岁巴西超模脸,高颧骨,健康古铜色肌肤"
|
- "23岁法式慵懒女模,法式刘海,微卷棕发,通透红唇"
|
||||||
- "33岁美式大女主气场,利落大波浪,深色红唇"
|
- "27岁欧美自然系女模,淡雀斑,大地色系淡妆,健康好气色" # 新增
|
||||||
- "22岁Y2K千禧辣妹,银色眼影,浅色唇彩"
|
- "21岁迈阿密热辣女孩,金棕色大波浪,高光立体修容,健康古铜色肌肤"
|
||||||
- "28岁美式复古Pin-up女孩, Victory卷发,饱满红唇"
|
- "24岁北美学院风女模,高马尾,通透元气妆,白皙肌肤"
|
||||||
- "24岁欧美高级中性模,凌乱短发,苍白质肌肤"
|
- "34岁北美宝妈系女模,自然淡妆,栗色长发,亲切温暖的微笑" # 新增
|
||||||
- "20岁加州冲浪系男孩,阳光金发,麦色健康肌"
|
|
||||||
|
# ========== 女模 - 欧美/街头/个性类(适合街头风、美式复古印花) ==========
|
||||||
|
- "22岁纽约下城区酷女孩,挑染发色,清冷厌世感,苍白质肌肤"
|
||||||
|
- "28岁欧美高级中性模,凌乱短发,冷白皮,骨感清冷"
|
||||||
|
- "26岁拉美混血风女模,深邃眼窝,野生挑眉,健康小麦色肌肤"
|
||||||
|
- "25岁Y2K千禧辣妹,银色挑染发,清透裸妆,微胖立体感"
|
||||||
|
|
||||||
|
# ========== 女模 - 极简/性冷淡/高街类(适合纯色基础款、卫衣) ==========
|
||||||
|
- "30岁北欧极简风女模,冷白皮,无瑕哑光肌,银色利落短发"
|
||||||
|
- "29岁纽约高街潮流女模,冷感通透妆,深棕色直发,疏离眼神"
|
||||||
|
- "31岁哥本哈根极简风设计师女模,利落黑色短发,黑白灰冷感气质"
|
||||||
|
- "28岁法式先锋艺术系女模,单边耳坠,冷感素颜,深金色发"
|
||||||
|
|
||||||
|
# ========== 女模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备) ========== # 新增分组
|
||||||
|
- "26岁非裔自然卷女模,健康深色肌肤,光泽感底妆,明快笑容"
|
||||||
|
- "29岁非裔都市丽人女模,利落编发,精致底妆,自信干练气场"
|
||||||
|
- "24岁印度裔女模,浓密黑长发,深邃大眼,暖调蜜糖色肌肤"
|
||||||
|
- "27岁拉丁裔阳光女模,栗色卷发,蜜色肌肤,热情爽朗笑容"
|
||||||
|
- "32岁中东裔优雅女模,微卷黑发,轮廓深邃,端庄知性气质"
|
||||||
|
- "28岁大码自信女模,健康微胖体型,自然光泽肌,开朗感染力笑容" # 大码:覆盖plus size市场
|
||||||
|
|
||||||
|
male:
|
||||||
|
# ========== 男模 - 亚洲/盐系/清新类(适合基础款、日韩系) ==========
|
||||||
|
- "23岁日系盐系男模,单眼皮,清瘦骨感,柔软黑发"
|
||||||
|
- "25岁亚洲清冷系男模,冷白皮,碎发自热感,下颌线清晰"
|
||||||
|
- "28岁亚洲轻熟男模,干净利落的短发,健康肤色,稳重气质"
|
||||||
|
- "21岁亚洲阳光男大,通透底妆,微卷黑发,清爽笑容"
|
||||||
|
|
||||||
|
# ========== 男模 - 欧美/街头/运动类(适合美式复古、街头印花) ==========
|
||||||
|
- "20岁加州冲浪系男模,阳光金发,麦色健康肌,清爽笑容"
|
||||||
|
- "24岁欧美健身教练型男,肌肉线条分明,小麦色肌肤,硬朗骨相"
|
||||||
|
- "22岁纽约街头滑板男模,脏辫,清冷厌世感,苍白肌肤"
|
||||||
|
- "27岁美式机车硬汉男模,粗犷髂廓,络腮胡,硬挺气质"
|
||||||
|
- "35岁北美成熟奶爸系男模,浅胡茬,栗色短发,温暖亲切笑容" # 新增
|
||||||
|
|
||||||
|
# ========== 男模 - 欧美/商务/雅痞类(适合重磅纯色T恤、卫衣) ==========
|
||||||
- "25岁英伦雅痞型男,复古油头,短胡茬,熟男气质"
|
- "25岁英伦雅痞型男,复古油头,短胡茬,熟男气质"
|
||||||
- "28岁意大利型男,深邃五官,意式凌乱卷发"
|
- "30岁华尔街商务精英男模,背头,沉稳内敛气质,干净皮肤"
|
||||||
- "22岁纽约街头滑板男孩,脏辫,宽松街头风"
|
- "28岁意大利型男,深邃五官,意式凌乱卷发,健康肌肤"
|
||||||
- "30岁华尔街商务精英,背头,沉稳内敛气质"
|
- "32岁法式儒雅大叔,微卷发,金丝眼镜,温润如玉气质"
|
||||||
- "26岁日系盐系清冷男模,单眼皮,清瘦骨感"
|
|
||||||
- "24岁欧美健身教练型男,肌肉线条分明,小麦色肌肤"
|
# ========== 男模 - 极简/暗黑/高街类(适合纯色基础款、暗黑风) ==========
|
||||||
- "29岁柏林暗黑系青年,苍白皮肤,银色配饰,冷感打底"
|
- "25岁欧美高冷男模,银白发色,苍白冷白皮,锋利眼神"
|
||||||
- "21岁英伦贵族少爷,金丝眼镜,白皙皮肤,忧郁气质"
|
- "29岁柏林暗黑系青年,苍白皮肤,冷感眼神,黑色直发"
|
||||||
- "27型美式机车硬汉,粗犷轮廓,络腮胡,硬挺气质"
|
- "26岁东欧赛博风男模,高颧骨,清冷骨相,浅色唇"
|
||||||
- "23岁东欧赛博朋克风骇客,机械感配饰,冷感眼妆"
|
- "33岁北欧性冷淡风男艺术家,银色短发,疏离眼神,极简气质"
|
||||||
- "32岁法式儒雅大叔,微卷发,眼镜,温润如玉气质"
|
|
||||||
- "25岁欧美高冷男模,银白发色,苍白冷白皮"
|
# ========== 男模 - 多元族裔类(适合多站点覆盖:北美/欧洲站必备) ========== # 新增分组
|
||||||
- "22岁俄裔冷酷超模,高颧骨,锋利眼神"
|
- "27岁非裔阳光型男模,健康深色肌肤,利落寸头,爽朗笑容"
|
||||||
- "26岁拉美混血风男模,深邃眼窝,健康小麦色"
|
- "31岁非裔商务型男模,修剪整齐的短须,挺拔身姿,可靠稳重气场"
|
||||||
- "28岁纽约高街潮流主理人,oversize穿搭,厌世脸"
|
- "26岁拉丁裔热情男模,微卷黑发,蜜色肌肤,明亮笑容"
|
||||||
- "20岁荷兰清新少女,雀斑,通透素颜"
|
- "29岁印度裔斯文男模,黑短发,浓眉,温和儒雅气质"
|
||||||
- "28岁北欧美人鱼风,长发及腰,冷感通透妆"
|
- "30岁中东裔深邃男模,黑色微卷发,轮廓立体,沉稳内敛"
|
||||||
- "24岁巴黎左岸文艺女青年,条纹衫气质,伪素颜"
|
- "33岁大码健硕男模,魁梧厚实体型,健康肤色,豪爽亲和笑容" # 大码:覆盖plus size市场
|
||||||
- "29岁米兰奢华贵妇,戴墨镜,精致法式美甲"
|
|
||||||
- "21岁加州 Coachella 音乐节女孩,波西米亚编发,晒伤妆"
|
|
||||||
- "27岁东欧废土风流浪者,做旧皮革质感,沧桑眼神"
|
|
||||||
- "23岁巴西里约热内卢狂欢女孩,羽毛头饰,高饱和妆容"
|
|
||||||
- "31岁哥本哈根极简风设计师,利落短发,黑白灰穿搭"
|
|
||||||
- "25岁美式复古绅士,圆框眼镜,马甲三件套"
|
|
||||||
- "22岁迈阿密热辣女孩,大波浪,高光立体修容妆"
|
|
||||||
- "26岁巴黎先锋艺术系青年,高领毛衣,单边耳坠"
|
|
||||||
- "32岁意式慵懒波西米亚女性,大波浪,流苏配饰"
|
|
||||||
- "25岁柏林冷酷电子乐DJ,全黑穿搭,荧光眼线"
|
|
||||||
- "20岁美式青春啦啦队队长,马尾,元气通透妆"
|
|
||||||
- "28岁伦敦复古古着店主理人,复古丝绒材质,红棕唇"
|
|
||||||
- "24岁北欧极简主义建筑系学生,眼镜,素雅气质"
|
|
||||||
- "29岁好莱坞黄金时代复古女星,手推波浪卷,红唇"
|
|
||||||
- "21岁加拿大户外徒步青年,冲锋衣,健康小麦色"
|
|
||||||
- "33岁法式波尔多酒庄主理人,优雅气质,微醺红唇"
|
|
||||||
- "26岁纽约苏荷区独立摄影师,工装背带裤,随性素颜"
|
|
||||||
- "30岁北欧性冷淡风艺术家,银色短发,疏离眼神"
|
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- australian animals
|
- australian animals
|
||||||
- beach house retro
|
- beach house retro
|
||||||
- australian minimal
|
- australian minimal
|
||||||
|
- australian emu
|
||||||
|
- australian cockatoo
|
||||||
|
- kookaburra
|
||||||
|
- platypus
|
||||||
|
- wombat
|
||||||
|
- tasmanian devil
|
||||||
|
- australian crocodile
|
||||||
|
- australian whale
|
||||||
|
- australian dolphin
|
||||||
|
- australian sea turtle
|
||||||
|
- australian reef fish
|
||||||
|
- australian jellyfish
|
||||||
|
- australian seahorse
|
||||||
|
- australian octopus
|
||||||
|
- australian coral
|
||||||
|
- australian mangrove
|
||||||
|
- australian rainforest
|
||||||
|
- australian waterfall
|
||||||
|
- australian canyon
|
||||||
|
- australian outback stars
|
||||||
|
- australian wildflower
|
||||||
|
- australian wattle
|
||||||
|
- australian gum tree
|
||||||
|
- australian fern
|
||||||
|
- australian surfboard
|
||||||
|
- australian boomerang
|
||||||
|
- australian retro car
|
||||||
|
- australian retro train
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- australia day fireworks
|
||||||
|
- easter bilby
|
||||||
|
- summer christmas
|
||||||
|
- beach santa
|
||||||
|
- new year sydney fireworks
|
||||||
|
- anzac poppy
|
||||||
|
- spring racing carnival
|
||||||
|
- surf lifesaving
|
||||||
|
- autumn leaves
|
||||||
|
- winter wildlife
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- brazilian landscape
|
- brazilian landscape
|
||||||
- brazilian summer
|
- brazilian summer
|
||||||
- brazilian retro design
|
- brazilian retro design
|
||||||
|
- brazilian macaw
|
||||||
|
- brazilian hummingbird
|
||||||
|
- brazilian butterfly
|
||||||
|
- brazilian monkey
|
||||||
|
- brazilian capybara
|
||||||
|
- brazilian sloth
|
||||||
|
- brazilian dolphin
|
||||||
|
- brazilian sea turtle
|
||||||
|
- brazilian reef fish
|
||||||
|
- brazilian coral
|
||||||
|
- brazilian waterfall
|
||||||
|
- brazilian river
|
||||||
|
- brazilian surf
|
||||||
|
- brazilian football retro
|
||||||
|
- brazilian carnival retro
|
||||||
|
- brazilian samba retro
|
||||||
|
- brazilian music retro
|
||||||
|
- brazilian guitar
|
||||||
|
- brazilian percussion
|
||||||
|
- brazilian dance
|
||||||
|
- brazilian capoeira
|
||||||
|
- brazilian street market
|
||||||
|
- brazilian fruit
|
||||||
|
- brazilian mango
|
||||||
|
- brazilian pineapple
|
||||||
|
- brazilian coconut
|
||||||
|
- brazilian palm
|
||||||
|
- brazilian coffee
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- carnival parade
|
||||||
|
- carnival feathers
|
||||||
|
- june festival
|
||||||
|
- tropical christmas
|
||||||
|
- new year beach
|
||||||
|
- easter eggs
|
||||||
|
- halloween pumpkin
|
||||||
|
- children's day
|
||||||
|
- samba festival
|
||||||
|
- autumn harvest
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- canadian nature
|
- canadian nature
|
||||||
- canadian wildlife retro
|
- canadian wildlife retro
|
||||||
- canadian outdoor
|
- canadian outdoor
|
||||||
|
- canadian beaver
|
||||||
|
- canadian goose
|
||||||
|
- canadian owl
|
||||||
|
- canadian eagle
|
||||||
|
- canadian black bear
|
||||||
|
- canadian wolf
|
||||||
|
- canadian fox
|
||||||
|
- canadian deer
|
||||||
|
- canadian elk
|
||||||
|
- canadian caribou
|
||||||
|
- canadian salmon
|
||||||
|
- canadian lobster
|
||||||
|
- canadian whale
|
||||||
|
- canadian puffin
|
||||||
|
- canadian heron
|
||||||
|
- canadian maple syrup
|
||||||
|
- canadian curling
|
||||||
|
- canadian skiing
|
||||||
|
- canadian ice skating
|
||||||
|
- canadian winter retro
|
||||||
|
- canadian waterfall
|
||||||
|
- canadian glacier
|
||||||
|
- canadian lighthouse
|
||||||
|
- canadian log cabin
|
||||||
|
- canadian barn
|
||||||
|
- canadian wheat
|
||||||
|
- canadian wildflower
|
||||||
|
- canadian pine
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- canada day fireworks
|
||||||
|
- thanksgiving harvest
|
||||||
|
- maple sugar shack
|
||||||
|
- winter carnival
|
||||||
|
- easter eggs
|
||||||
|
- halloween pumpkin
|
||||||
|
- christmas snow
|
||||||
|
- new year fireworks
|
||||||
|
- autumn leaves
|
||||||
|
- spring blossom
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- german minimal design
|
- german minimal design
|
||||||
- german coastal
|
- german coastal
|
||||||
- german retro typography
|
- german retro typography
|
||||||
|
- german owl
|
||||||
|
- german fox
|
||||||
|
- german deer
|
||||||
|
- german hare
|
||||||
|
- german rabbit
|
||||||
|
- german squirrel
|
||||||
|
- german hedgehog
|
||||||
|
- german badger
|
||||||
|
- german stork
|
||||||
|
- german robin
|
||||||
|
- german swan
|
||||||
|
- german heron
|
||||||
|
- german kingfisher
|
||||||
|
- german woodpecker
|
||||||
|
- german forest animals
|
||||||
|
- german alpine flowers
|
||||||
|
- german edelweiss
|
||||||
|
- german cornflower
|
||||||
|
- german poppy
|
||||||
|
- german sunflower
|
||||||
|
- german lavender
|
||||||
|
- german rose
|
||||||
|
- german tulip
|
||||||
|
- german wildflowers
|
||||||
|
- german meadow
|
||||||
|
- german apple orchard
|
||||||
|
- german pumpkin
|
||||||
|
- german strawberry
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- easter eggs
|
||||||
|
- christmas market
|
||||||
|
- advent calendar
|
||||||
|
- lantern parade
|
||||||
|
- carnival costume
|
||||||
|
- maypole
|
||||||
|
- autumn harvest
|
||||||
|
- new year fireworks
|
||||||
|
- halloween pumpkin
|
||||||
|
- spring blossom
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- spanish retro poster travel
|
- spanish retro poster travel
|
||||||
- spanish mountains retro
|
- spanish mountains retro
|
||||||
- spanish art
|
- spanish art
|
||||||
|
- spanish horse
|
||||||
|
- spanish donkey
|
||||||
|
- spanish goat
|
||||||
|
- spanish sheep
|
||||||
|
- spanish cow
|
||||||
|
- spanish castanets
|
||||||
|
- spanish fan
|
||||||
|
- spanish mantilla
|
||||||
|
- spanish shawl
|
||||||
|
- spanish lace
|
||||||
|
- spanish embroidery
|
||||||
|
- spanish mosaic
|
||||||
|
- spanish fountain
|
||||||
|
- spanish plaza
|
||||||
|
- spanish courtyard
|
||||||
|
- spanish balcony
|
||||||
|
- spanish arch
|
||||||
|
- spanish tower
|
||||||
|
- spanish castle
|
||||||
|
- spanish lighthouse
|
||||||
|
- spanish harbor
|
||||||
|
- spanish cove
|
||||||
|
- spanish orange
|
||||||
|
- spanish lemon
|
||||||
|
- spanish pomegranate
|
||||||
|
- spanish watermelon
|
||||||
|
- spanish churros
|
||||||
|
- spanish octopus
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- la tomatina
|
||||||
|
- spanish fiesta
|
||||||
|
- fallas festival
|
||||||
|
- flamenco festival
|
||||||
|
- new year grapes
|
||||||
|
- summer festival
|
||||||
|
- halloween pumpkin
|
||||||
|
- valentine hearts
|
||||||
|
- autumn harvest
|
||||||
|
- spring blossom
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- london architecture
|
- london architecture
|
||||||
- english tea party
|
- english tea party
|
||||||
- british minimal
|
- british minimal
|
||||||
|
- london double decker
|
||||||
|
- british seaside retro
|
||||||
|
- english country cottage
|
||||||
|
- british canal boats
|
||||||
|
- london telephone box
|
||||||
|
- british bulldog
|
||||||
|
- english rose
|
||||||
|
- british coastal cliffs
|
||||||
|
- london night skyline
|
||||||
|
- british punk retro
|
||||||
|
- english garden flowers
|
||||||
|
- london parks
|
||||||
|
- british retro car
|
||||||
|
- mini cooper retro
|
||||||
|
- british music festival
|
||||||
|
- english fox
|
||||||
|
- british garden birds
|
||||||
|
- scottish thistle
|
||||||
|
- welsh dragon
|
||||||
|
- british village green
|
||||||
|
- london architecture retro
|
||||||
|
- british retro typography
|
||||||
|
- british floral pattern
|
||||||
|
- british retro poster art
|
||||||
|
- english oak tree
|
||||||
|
- british beach hut
|
||||||
|
- british street market
|
||||||
|
- british retro train
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- bonfire night fireworks
|
||||||
|
- christmas crackers
|
||||||
|
- easter eggs
|
||||||
|
- summer bank holiday
|
||||||
|
- london new year fireworks
|
||||||
|
- halloween pumpkin
|
||||||
|
- valentine hearts
|
||||||
|
- autumn harvest
|
||||||
|
- spring blossom
|
||||||
|
- winter snow scene
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- italian folk patterns
|
- italian folk patterns
|
||||||
- italian minimal design
|
- italian minimal design
|
||||||
- italian art
|
- italian art
|
||||||
|
- italian wolf
|
||||||
|
- italian fox
|
||||||
|
- italian deer
|
||||||
|
- italian hare
|
||||||
|
- italian rabbit
|
||||||
|
- italian squirrel
|
||||||
|
- italian hedgehog
|
||||||
|
- italian owl
|
||||||
|
- italian falcon
|
||||||
|
- italian swallow
|
||||||
|
- italian peacock
|
||||||
|
- italian swan
|
||||||
|
- italian flamingo
|
||||||
|
- italian sea turtle
|
||||||
|
- italian octopus
|
||||||
|
- italian seahorse
|
||||||
|
- italian coral
|
||||||
|
- italian shell
|
||||||
|
- italian gondola
|
||||||
|
- italian sailboat
|
||||||
|
- italian lemon
|
||||||
|
- italian fig
|
||||||
|
- italian pomegranate
|
||||||
|
- italian tomato
|
||||||
|
- italian basil
|
||||||
|
- italian gelato
|
||||||
|
- italian cannoli
|
||||||
|
- italian focaccia
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- carnevale mask
|
||||||
|
- ferragosto summer
|
||||||
|
- new year fireworks
|
||||||
|
- easter eggs
|
||||||
|
- summer festival
|
||||||
|
- autumn harvest
|
||||||
|
- valentine hearts
|
||||||
|
- halloween pumpkin
|
||||||
|
- spring blossom
|
||||||
|
- winter snow
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- 五重塔
|
- 五重塔
|
||||||
- 和太鼓
|
- 和太鼓
|
||||||
- 風神雷神
|
- 風神雷神
|
||||||
|
- 花火大会
|
||||||
|
- 和傘
|
||||||
|
- 柴犬
|
||||||
|
- 秋田犬
|
||||||
|
- うさぎ
|
||||||
|
- 狐
|
||||||
|
- 狸
|
||||||
|
- 亀
|
||||||
|
- 蛍
|
||||||
|
- 蜻蛉
|
||||||
|
- 蝶
|
||||||
|
- カブトムシ
|
||||||
|
- フクロウ
|
||||||
|
- 燕
|
||||||
|
- 雀
|
||||||
|
- 松
|
||||||
|
- 椿
|
||||||
|
- 藤
|
||||||
|
- 紫陽花
|
||||||
|
- 朝顔
|
||||||
|
- 向日葵
|
||||||
|
- 銀杏
|
||||||
|
- 雪の結晶
|
||||||
|
- 七夕
|
||||||
|
- お月見
|
||||||
|
- 温泉
|
||||||
|
- 天守閣
|
||||||
|
- 忍者
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- お正月デザイン
|
||||||
|
- ひな祭り
|
||||||
|
- クリスマス
|
||||||
|
- ハロウィン
|
||||||
|
- 雪だるま
|
||||||
|
- 節分
|
||||||
|
- バレンタイン
|
||||||
|
- お花見
|
||||||
|
- こどもの日
|
||||||
|
- 大晦日
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- tropical birds
|
- tropical birds
|
||||||
- mexican handcraft
|
- mexican handcraft
|
||||||
- mexican minimal
|
- mexican minimal
|
||||||
|
- mexican monarch butterfly
|
||||||
|
- mexican hummingbird
|
||||||
|
- mexican coyote
|
||||||
|
- mexican wolf
|
||||||
|
- mexican fox
|
||||||
|
- mexican deer
|
||||||
|
- mexican armadillo
|
||||||
|
- mexican iguana
|
||||||
|
- mexican sea turtle
|
||||||
|
- mexican dolphin
|
||||||
|
- mexican whale
|
||||||
|
- mexican starfish
|
||||||
|
- mexican seahorse
|
||||||
|
- mexican octopus
|
||||||
|
- mexican aloe
|
||||||
|
- mexican yucca
|
||||||
|
- mexican avocado
|
||||||
|
- mexican chili
|
||||||
|
- mexican corn
|
||||||
|
- mexican tortilla
|
||||||
|
- mexican tamale
|
||||||
|
- mexican taco
|
||||||
|
- mexican guacamole
|
||||||
|
- mexican vanilla
|
||||||
|
- mexican piñata
|
||||||
|
- mexican maracas
|
||||||
|
- mexican huipil
|
||||||
|
- mexican rebozo
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- dia de los muertos
|
||||||
|
- cinco de mayo
|
||||||
|
- independence fiesta
|
||||||
|
- christmas posada
|
||||||
|
- new year fireworks
|
||||||
|
- easter eggs
|
||||||
|
- valentine hearts
|
||||||
|
- mother's day flowers
|
||||||
|
- summer fiesta
|
||||||
|
- autumn harvest
|
||||||
|
|||||||
@@ -41,3 +41,42 @@ seeds:
|
|||||||
- polish folk birds
|
- polish folk birds
|
||||||
- polish countryside retro
|
- polish countryside retro
|
||||||
- polish traditional patterns
|
- polish traditional patterns
|
||||||
|
- polish owl
|
||||||
|
- polish fox
|
||||||
|
- polish deer
|
||||||
|
- polish hare
|
||||||
|
- polish rabbit
|
||||||
|
- polish squirrel
|
||||||
|
- polish hedgehog
|
||||||
|
- polish badger
|
||||||
|
- polish stork
|
||||||
|
- polish crane
|
||||||
|
- polish swan
|
||||||
|
- polish kingfisher
|
||||||
|
- polish woodpecker
|
||||||
|
- polish bison
|
||||||
|
- polish wolf
|
||||||
|
- polish lynx
|
||||||
|
- polish forest animals
|
||||||
|
- polish wildflowers
|
||||||
|
- polish cornflower
|
||||||
|
- polish poppy
|
||||||
|
- polish sunflower
|
||||||
|
- polish meadow
|
||||||
|
- polish apple orchard
|
||||||
|
- polish strawberry
|
||||||
|
- polish mushroom
|
||||||
|
- polish pierogi
|
||||||
|
- polish gingerbread
|
||||||
|
- polish honey
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- easter eggs
|
||||||
|
- christmas market
|
||||||
|
- midsummer wreaths
|
||||||
|
- harvest festival
|
||||||
|
- valentine hearts
|
||||||
|
- halloween pumpkin
|
||||||
|
- new year fireworks
|
||||||
|
- autumn leaves
|
||||||
|
- winter snow
|
||||||
|
- spring blossom
|
||||||
|
|||||||
@@ -41,3 +41,40 @@ seeds:
|
|||||||
- saudi retro poster
|
- saudi retro poster
|
||||||
- desert oasis illustration
|
- desert oasis illustration
|
||||||
- arabic geometric art
|
- arabic geometric art
|
||||||
|
- arabian falcon
|
||||||
|
- arabian oryx
|
||||||
|
- arabian gazelle
|
||||||
|
- arabian wolf
|
||||||
|
- arabian fox
|
||||||
|
- arabian hare
|
||||||
|
- arabian goat
|
||||||
|
- arabian sheep
|
||||||
|
- arabian hoopoe
|
||||||
|
- arabian dove
|
||||||
|
- arabian peacock
|
||||||
|
- arabian flamingo
|
||||||
|
- arabian sea turtle
|
||||||
|
- arabian dolphin
|
||||||
|
- arabian whale
|
||||||
|
- arabian coral
|
||||||
|
- arabian starfish
|
||||||
|
- arabian seahorse
|
||||||
|
- arabian octopus
|
||||||
|
- arabian desert rose
|
||||||
|
- arabian jasmine
|
||||||
|
- arabian date palm
|
||||||
|
- arabian olive
|
||||||
|
- arabian pomegranate
|
||||||
|
- arabian mint
|
||||||
|
- arabian coffee
|
||||||
|
- arabian dates
|
||||||
|
- arabian carpet
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- desert festival
|
||||||
|
- camel racing
|
||||||
|
- date harvest
|
||||||
|
- arabian nights
|
||||||
|
- oasis festival
|
||||||
|
- red sea festival
|
||||||
|
- saudi coffee culture
|
||||||
|
- winter desert
|
||||||
|
|||||||
@@ -42,3 +42,42 @@ seeds:
|
|||||||
- baseball retro
|
- baseball retro
|
||||||
- basketball street
|
- basketball street
|
||||||
- hiking adventure
|
- hiking adventure
|
||||||
|
- bald eagle illustration
|
||||||
|
- route 66 road trip
|
||||||
|
- new york skyline
|
||||||
|
- golden gate bridge
|
||||||
|
- grand canyon sunset
|
||||||
|
- yellowstone wildlife
|
||||||
|
- rocky mountain peaks
|
||||||
|
- appalachian trail
|
||||||
|
- jazz age retro
|
||||||
|
- blues music retro
|
||||||
|
- rock and roll retro
|
||||||
|
- country music retro
|
||||||
|
- rodeo retro
|
||||||
|
- barn quilt patterns
|
||||||
|
- farmhouse retro
|
||||||
|
- american diner neon
|
||||||
|
- drive-in movie retro
|
||||||
|
- vintage soda pop
|
||||||
|
- american football retro
|
||||||
|
- skateboard deck art
|
||||||
|
- graffiti street art
|
||||||
|
- desert cactus
|
||||||
|
- retro roller skate
|
||||||
|
- american retro circus
|
||||||
|
- liberty bell retro
|
||||||
|
- space shuttle retro
|
||||||
|
- vintage pinball
|
||||||
|
- bowling retro
|
||||||
|
# —— 节日主题(适合印花,避开宗教人物/国旗/酒精/猪等敏感项)——
|
||||||
|
- 4th of july fireworks
|
||||||
|
- thanksgiving turkey
|
||||||
|
- autumn pumpkin harvest
|
||||||
|
- christmas snowman
|
||||||
|
- winter reindeer
|
||||||
|
- valentine hearts
|
||||||
|
- easter bunny
|
||||||
|
- halloween pumpkin
|
||||||
|
- new year fireworks
|
||||||
|
- st patrick shamrock
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 种草图提示词模板库 —— 跨境电商详情页专用版(稳定出图+零遮挡强约束版)
|
# 种草图提示词模板库 —— 跨境电商详情页专用版(稳定出图+零遮挡强约束+得体姿态+全亮调场景版)
|
||||||
# 占位符:[商品名称]、[材质]、[模特特征]、[服装风格]
|
# 占位符:[商品名称]、[材质]、[模特特征]、[服装风格]
|
||||||
# 摄影参数说明:35mm/50mm镜头控制自然透视,f/1.4-f/2.8控制景深虚化,8k强化面料细节,3:4符合电商标准长图比例。
|
# 摄影参数说明:35mm/50mm镜头控制自然透视,f/1.4-f/2.8控制景深虚化,8k强化面料细节,3:4符合电商标准长图比例。
|
||||||
|
|
||||||
@@ -8,165 +8,167 @@ seed_shot_templates:
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "纯白背景正面全展示"
|
- name: "纯白背景正面全展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,身体正对镜头微侧15度,眼神直视镜头,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,身体正对镜头微侧15度,眼神直视镜头,整体呈现[服装风格]的调性,完整展示衣服正面的印花图案与[材质]面料的垂坠感。采用电商标准的高亮柔光,左右各一盏柔光箱消除杂乱阴影,光线均匀分布。画面比例为3:4。商业白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "高级浅灰背景全展示"
|
- name: "高级浅灰背景全展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在高级浅灰背景纸前,身体正对镜头,姿态略微随性,整体呈现[服装风格]的调性,完整展示衣服正面印花与[材质]的挺括度。采用影棚标准柔光与边缘光结合。画面比例为3:4。商业白底摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "纯白背景双手展示下摆"
|
- name: "纯白背景双手展示下摆"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须背在身后或自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景半身构图,模特站在纯白背景纸前,眼神直视镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手背在身后或自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景半身构图,模特站在纯白背景纸前,眼神直视镜头,整体呈现[服装风格]的调性,画面聚焦展示衣服正面印花图案与下摆的缝线工艺。采用影棚高显色柔光箱,突出印花色彩与[材质]的紧密编织纹理。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "低饱和克莱因蓝背景棚拍"
|
- name: "低饱和克莱因蓝背景棚拍"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特在低饱和克莱因蓝的无缝背景纸前摆出具有张力的站姿,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特在低饱和克莱因蓝的无缝背景纸前摆出具有张力的站姿,身体正对镜头,整体呈现[服装风格]的调性,突出衣服正面印花图案的视觉冲击力与剪裁,展示[材质]的独特纹理。光线采用双灯硬光与柔光箱结合,形成干净分明的明暗对比。画面比例为3:4。极简主义商业摄影,85mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "纯白背景微动态走动展示"
|
- name: "纯白背景微动态走动展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,呈现正对镜头的动态走动状态,整体呈现[服装风格]的调性,展示衣服正面印花在动态下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在纯白无缝背景纸前,呈现正对镜头的微动态走动状态,整体呈现[服装风格]的调性,展示衣服正面印花在动态下的视觉效果与[材质]面料的动态垂坠感。采用影棚高亮柔光,光比干净分明。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 二、 面料与印花特写区(打消购买顾虑)
|
# 二、 面料与印花特写区(打消购买顾虑)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "纯白背景领口细节特写"
|
- name: "纯白背景领口细节特写"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示衣服的领口、袖口等工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯白背景纸前,整体呈现[服装风格]的调性,画面聚焦展示衣服的领口、袖口等工艺细节与[材质]面料的紧密编织纹理。采用影棚标准高显色柔光箱,光线均匀且突出材质的高密度纹理与亲肤特性。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "逆光面料透气质感展示"
|
- name: "明亮室内逆光面料透气质感展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特在黄昏时分的空旷天台逆光站立,发丝被风吹起,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在逆光强光下展现出的[材质]面料透光性与柔软质感。光线为日落黄金时刻逆光,边缘形成强烈的轮廓光发丝发亮。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特在阳光明媚的玻璃幕墙大堂内逆光站立,发丝被自然光照亮,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服在明亮逆光下展现出的[材质]面料透光性与柔软质感。光线为正午明亮的自然逆光,边缘形成强烈的轮廓光。画面比例为3:4。清透氧气感摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "双手拉扯领口弹性展示"
|
- name: "双手拉扯领口弹性展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手捏住领口两侧向外轻拉,展示衣服的弹力与防变形属性,绝对不可将手放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捏住领口两侧向外轻拉展示弹力,绝对不可将手放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中近景构图,模特站在纯色背景前,凸显[材质]面料的回弹性与结实度。采用高亮柔光,突出面料拉伸时的纹理张力。画面比例为3:4。商业电商摄影,50mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "低头凝视正面印花特写"
|
- name: "低头凝视正面印花特写"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特站在纯白背景纸前,微微低头凝视衣服胸前的印花图案,整体呈现[服装风格]的调性,画面以斜俯视角度聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特站在纯白背景纸前,微微低头凝视衣服胸前的印花图案,整体呈现[服装风格]的调性,画面以斜俯视角度聚焦展示正面印花的色彩细节与[材质]面料的质感。采用影棚顶部柔光与正面补光,突出印花图案的清晰度。画面比例为3:4。电商白底图摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 三、 居家生活场景区(营造松弛感与舒适度)
|
# 三、 居家生活场景区(营造松弛感与舒适度)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "居家沙发慵懒场景"
|
- name: "居家沙发坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中远景全身构图,模特慵懒地靠在现代极简风格的客厅布艺沙发上,身体正对镜头微侧,姿态松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的舒适度。室内采用柔和的漫反射自然光,背景为高度虚化的居家环境。画面比例为3:4。柯达Portra 400胶片质感,色彩柔和,35mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手自然放在腿上或沙发扶手上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特得体地坐在明亮现代极简风格的客厅布艺沙发上,身体正对镜头微侧,姿态松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的舒适度。室内采用柔和的高调漫反射自然光,背景为高度虚化的明亮居家环境。画面比例为3:4。柯达Portra 400胶片质感,色彩柔和明亮,35mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "居家落地窗自然光展示"
|
- name: "居家落地窗自然光展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特站在高层公寓的落地窗前,单手轻触玻璃,身体正前方对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日落前的柔和逆光,形成强烈的轮廓光发丝发亮。背景为大虚化的城市楼宇剪影与暖色光晕。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在高层公寓明亮的落地窗前,身体正前方对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]亲肤舒适属性。采用日间明亮的柔和逆光,画面通透。背景为大虚化的明亮城市楼宇与蓝天。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "周末清晨床头场景"
|
- name: "周末清晨床头坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂或撑在身后,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景构图,模特坐在柔软的双人床上伸懒腰,展现周末清晨的松弛感,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的亲肤无拘束感。室内采用清晨柔和的侧向自然光,背景为高度虚化的卧室家具。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手自然放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特得体地坐在床边,展现周末清晨的松弛感,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的亲肤无拘束感。室内采用清晨明亮的高调自然光,背景为高度虚化的卧室家具。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "居家厨房烹饪场景"
|
- name: "居家厨房场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂或单手操作厨具,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中远景全身构图,模特站在现代开放式厨房的中岛台前切水果,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手操作厨具,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中远景全身构图,模特站在现代开放式厨房的中岛台前,身体正对镜头微侧,姿态放松自然,整体呈现[服装风格]的调性,展示衣服正面印花在居家生活中的百搭属性与[材质]面料的舒适耐穿度。室内采用明亮的漫反射自然光与暖色室内灯,背景为高度虚化的厨房台面与绿植。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "周末居家阅读场景"
|
- name: "周末居家阅读场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须捧书放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特盘腿坐在卧室的羊毛地毯上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料的柔软亲肤感。室内采用柔和的窗光侧逆光,背景为高度虚化的卧室床铺与暖色氛围灯。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手必须捧书放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特盘腿坐在卧室的羊毛地毯上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料的柔软亲肤感。室内采用柔和的窗光侧逆光,背景为高度虚化的卧室床铺与暖色氛围灯。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 四、 户外休闲/运动场景区(展现百搭与透气)
|
# 四、 户外休闲/运动场景区(展现百搭与透气)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "阳光草坪户外实拍"
|
- name: "阳光草坪户外站姿"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身动态抓拍构图,模特行走在阳光斑驳的城市林荫道上,身体正对镜头微侧,微微低头微笑,呈现不经意的随性状态,整体展现[服装风格]的氛围,凸显[材质]在自然行动中的透气与百搭。午后阳光透过树叶洒下丁达尔光斑与树影,背景为高度虚化的过往行人与都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度色彩,35mm镜头,f/1.7大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的城市林荫道上,身体正对镜头微侧,微微低头微笑,整体展现[服装风格]的氛围,凸显[材质]在自然行动中的透气与百搭。午后阳光透过树叶洒下明亮的光斑,背景为高度虚化的过往行人与都市街景。画面比例为3:4。徕卡Q2摄影质感,高对比度明亮色彩,35mm镜头,f/1.7大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "都市街头OOTD穿搭展示"
|
- name: "都市街头OOTD站姿展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂或单手拿咖啡杯放在腰侧,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在现代商业街区或美术馆外墙前,身体正对镜头微靠在墙上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手拿咖啡杯放在腰侧,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的现代商业街区外墙前,身体正对镜头微靠在墙上,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱性。采用午后天光与建筑反射光,画面明亮通透,背景为高度虚化的都市街景与阳光光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "天台蓝天白云清透场景"
|
- name: "天台蓝天白云站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光,无死角的展现服装颜色。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在无遮挡的空旷天台上,背景是大面积的蓝天白云,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气感。正午明亮的顺光,无死角的展现服装颜色。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "自然公园休闲外景"
|
- name: "自然公园站姿外景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在阳光充足的公园草坪前,微风吹拂发丝,微微仰头感受自然,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光,画面明亮通透,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光充足的公园草坪前,微风吹拂发丝,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性与轻盈感。光线为明亮的顺光搭配自然反光,画面明亮通透,背景为大面积虚化的鲜绿植被与光斑。画面比例为3:4。清透氧气感色彩,索尼A7R4画质,85mm镜头,f/1.8光圈,清新高调摄影,8k分辨率。
|
||||||
|
|
||||||
- name: "街头滑板运动场景"
|
- name: "街头滑板站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身动态抓拍构图,模特单脚踩着滑板停在滑板公园的U型池边缘,身体正对镜头,微风吹拂发丝,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。午后强烈的阳光形成侧逆光,背景为高度虚化的水泥滑板池与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/1.7光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚踩在滑板上,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在阳光明媚的滑板公园边缘,身体正对镜头,微风吹拂发丝,呈现不经意的随性状态,整体展现[服装风格]的氛围,展示衣服正面印花与[材质]的运动属性。明亮的阳光形成侧逆光,背景为高度虚化的彩色滑板场地与涂鸦墙。画面比例为3:4。徕卡Q2摄影质感,35mm镜头,f/1.7光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "阳光林荫道骑行场景"
|
- name: "林荫道骑行停靠场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须握把且手肘外展不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身动态抓拍构图,模特骑着一辆复古自行车停在两旁长满梧桐树的林荫小道上,单脚点地,转头看向镜头微笑,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性。午后阳光透过树叶洒下丁达尔光斑,背景为高度虚化的树干与光斑。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,单脚点地双手握把,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特骑着一辆复古自行车停在两旁长满梧桐树的林荫小道上,转头看向镜头微笑,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的透气性。明亮阳光透过树叶洒下光斑,背景为高度虚化的树干与明亮光斑。画面比例为3:4。日系胶片质感,柯达Portra 400色彩预设,40mm镜头,f/2.0光圈,8k分辨率。
|
||||||
- name: "海边漫步度假场景"
|
|
||||||
prompt: |
|
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然张开或下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身动态抓拍构图,模特走在细软的沙滩上,海风吹起衣摆,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,画面明亮高调,背景为高度虚化的蔚蓝海水、白沙滩与远处海平线。画面比例为3:4。索尼A7R4画质,35mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。
|
|
||||||
|
|
||||||
- name: "露营帐篷森系户外场景"
|
- name: "海边沙滩站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须端着搪瓷杯放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特坐在森林空地的露营椅上,旁边有燃烧的篝火和搭好的金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的保暖性。采用黄昏暖色篝火光与天光交织,背景为高度虚化的茂密松林与烟雾。画面比例为3:4。复古胶片质感,35mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在细软的沙滩上,海风吹起衣摆,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与夏日属性。光线为正午强烈的顺光,画面明亮高调,背景为高度虚化的蔚蓝海水、白沙滩与远处海平线。画面比例为3:4。索尼A7R4画质,35mm镜头,f/2.0光圈,高饱和度清透色彩,8k分辨率。
|
||||||
|
|
||||||
- name: "海风度假场景展示"
|
- name: "露营帐篷坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然张开,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特赤脚走在浅水沙滩上,海风吹起衣摆,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与飘逸感。光线为正午到下午的明亮阳光,海面反光形成自然补光,背景为高度虚化的蔚蓝大海、白沙滩与天空。画面比例为3:4。夏日清透色彩,高饱和度,35mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手端着搪瓷杯放在腿上,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特坐在森林空地的露营椅上,旁边有搭好的白色金字塔帐篷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的户外属性。采用正午明亮的自然光,背景为高度虚化的茂密松林与光斑。画面比例为3:4。复古胶片质感,35mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "热带绿植温室场景"
|
- name: "浅水沙滩站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在长满大型龟背竹与天堂鸟的植物园温室中,手指轻轻触碰绿叶,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与透气感。光线为穿透植物叶片的柔和自然光,形成斑驳的树影打在衣服上,背景为高度虚化的茂密绿植与光斑。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.4大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤,脚上穿着休闲鞋。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在浅水沙滩边,海风吹起衣摆,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与飘逸感。光线为正午到下午的明亮阳光,海面反光形成自然补光,背景为高度虚化的蔚蓝大海、白沙滩与天空。画面比例为3:4。夏日清透色彩,高饱和度,35mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "阴天极简冷淡风外景"
|
- name: "热带绿植温室站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特背靠在粗犷的混凝土墙面或现代美术馆外墙前,眼神冷酷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的街头属性。采用阴天柔和的漫反射自然冷光,背景为低饱和的莫兰迪灰调与几何阴影,画面干净清冷。画面比例为3:4。极简主义商业摄影,55mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在长满大型龟背竹与天堂鸟的植物园温室中,笑容明朗,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的轻盈与透气感。光线为穿透玻璃顶的明亮自然光,形成明亮的光斑,背景为高度虚化的茂密绿植。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.4大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "山顶自然风光征服场景"
|
- name: "现代美术馆纯白站姿外景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须叉腰但不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在山顶的岩石上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用傍晚日落前的逆光与山顶漫反射光,背景为高度虚化的连绵山脉与云海。画面比例为3:4。电影级情绪摄影,35mm镜头,f/2.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代美术馆纯白外墙前,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的极简高级感。采用明亮的顺光漫反射,背景为低饱和的明亮白调与几何光影,画面干净清透。画面比例为3:4。极简主义商业摄影,55mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
|
- name: "山顶站姿场景"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在山顶的岩石上,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花图案与[材质]面料在户外的耐穿与透气属性。采用正午明亮的顺光,画面通透,背景为高度虚化的连绵山脉与蓝天白云。画面比例为3:4。电影级情绪摄影,35mm镜头,f/2.8光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 五、 工作/通勤场景区(展现日常实用度)
|
# 五、 工作/通勤场景区(展现日常实用度)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "办公场景商务休闲展示"
|
- name: "办公场景坐姿展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须撑在下巴上且不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特坐在极简办公桌前,身体正对镜头微侧,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的现代白光照明,背景为高度虚化的电脑屏幕与办公桌绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手放在桌上或键盘上不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮的极简办公桌前,身体正对镜头微侧,展现干练气质,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的抗皱与挺括感。室内采用明亮的高调白光照明,背景为高度虚化的电脑屏幕与办公桌绿植,画面通透干净。画面比例为3:4。商业电商摄影,50mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "咖啡馆周末休闲场景"
|
- name: "咖啡馆坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,单手端着陶瓷咖啡杯放在嘴边不遮挡印花,另一只手自然下垂,绝对不可双手交叉抱胸。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特坐在极简风格咖啡馆的窗边座位上,身体正对镜头微侧,姿态慵懒松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的柔软垂坠感。晨间自然光从大窗户斜射进来形成柔和侧逆光,背景为高度虚化的木质室内装潢、绿植与朦胧的咖啡蒸汽。画面比例为3:4。日系胶片质感,富士Provia 400X色彩预设,35mm镜头,f/1.8大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,单手端着陶瓷咖啡杯放在嘴边不遮挡印花,另一只手自然放在桌上,绝对不可双手交叉抱胸。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在明亮风格咖啡馆的窗边座位上,身体正对镜头微侧,姿态慵懒松弛,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的柔软垂坠感。明亮的自然光从大窗户斜射进来,画面通透,背景为高度虚化的木质室内装潢、绿植与朦胧的咖啡蒸汽。画面比例为3:4。日系胶片质感,富士Provia 400X色彩预设,35mm镜头,f/1.8大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "书店翻阅书籍场景"
|
- name: "书店站姿翻阅场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须捧书放在腰间不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特站在高大的木质书架前翻阅一本精装书,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用暖色调的钨丝灯点光源,背景为高度虚化的书脊与暖色光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手捧书放在腰间不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特站在明亮的高大木质书架前翻阅一本精装书,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括与质感。书店采用明亮的射灯与自然光交织,背景为高度虚化的书脊与明亮光斑。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "复古家具店探店场景"
|
- name: "复古家具店站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在摆满上世纪老物件的中古家具店内,手指轻抚一把复古单椅,眼神安静,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的质感。店内采用暖色调的钨丝灯漫反射,背景为高度虚化的胡桃木柜子与老式台灯。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在摆满上世纪老物件的中古家具店内,眼神安静,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的质感。店内采用明亮的自然光漫反射,背景为高度虚化的胡桃木柜子与明亮光斑。画面比例为3:4。富士Provia 400X色彩预设,35mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "艺术画廊看展高级场景"
|
- name: "艺术画廊站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在灯光柔和的现代艺术画廊内,侧身凝视一幅巨大的抽象画后转过头来正对镜头,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业的顶光灯打出柔和的漫反射,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,35mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在灯光柔和明亮的现代艺术画廊内,侧身凝视一幅巨大的抽象画后转过头来正对镜头,气质疏离高级,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]的高级调性。画廊专业的顶光灯打出柔和的漫反射,背景为高度虚化的洁白墙面与画框。画面比例为3:4。索尼A7R4画质,35mm镜头,f/1.8大光圈,清冷调高级感,8k分辨率。
|
||||||
|
|
||||||
- name: "超市货架色彩碰撞场景"
|
- name: "超市货架站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须推车不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特推着购物车或站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,姿态俏皮个性,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的冷光源打亮环境,背景为高度虚化的彩色商品与灯轨。画面比例为3:4。赛博潮流摄影,35mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手推车不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特推着购物车或站在摆满色彩鲜艳饮料的超市货架前,身体正对镜头,姿态俏皮个性,整体呈现[服装风格]的调性,展示衣服正面印花图案与背景色彩的碰撞感及[材质]的日常实穿度。货架顶部的明亮冷光源打亮环境,背景为高度虚化的彩色商品与灯轨。画面比例为3:4。赛博潮流摄影,35mm镜头,f/1.8光圈,8k分辨率。
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 六、 情绪氛围与夜景场景区(提升品牌调性)
|
# 六、 情绪氛围与高亮特色场景区(提升品牌调性)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: "工业风工作室背景展示"
|
- name: "明亮工业风咖啡馆站姿展示"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特站在工业风loft工作室的水泥墙前,眼神清冷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用顶部的冷色聚光灯与微弱的暖色环境光对比,背景为高度虚化的昏暗通道与反光地面。画面比例为3:4。赛博潮流电商摄影,55mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂或单手插兜,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特站在明亮工业风咖啡馆的纯白水泥墙前,眼神清冷,身体正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的硬挺与廓形。采用正午明亮的自然光与室内暖光交织,背景为高度虚化的明亮通道与绿植。画面比例为3:4。赛博潮流电商摄影,55mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "录音棚音乐人场景"
|
- name: "录音棚站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须调音不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景构图,模特戴着监听耳机站在专业麦克风前调音,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用录音棚特有的暖色小聚光灯与整体冷色调对比,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手调音不遮挡胸口,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景构图,模特戴着监听耳机站在专业麦克风前调音,眼神专注,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的挺括廓形。采用录音棚明亮的暖色聚光灯与整体白光对比,背景为高度虚化的吸音棉墙面与调音台。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.8光圈,8k分辨率。
|
||||||
- name: "微醺暖光清吧场景"
|
|
||||||
prompt: |
|
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须把玩酒杯放在吧台上不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。中景半身构图,模特坐在氛围感清吧的吧台前,眼神微醺迷离,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用暖黄色的点光源与背景微弱的蓝光对比,背景为高度虚化的酒瓶阵列与杯光交错。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。
|
|
||||||
|
|
||||||
- name: "夜晚霓虹街头场景"
|
- name: "暖光清吧坐姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须插兜不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特夜晚倚靠在城市天桥栏杆旁,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在夜色下的视觉效果与[材质]的质感与垂感。采用强烈的边缘光与城市霓虹灯的彩色反光(青蓝与橘红对比),背景为高度虚化的车流光轨与霓虹灯牌。画面比例为3:4。赛博朋克潮流摄影,35mm镜头,f/1.4大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的坐姿,双手把玩酒杯放在吧台上不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。中景半身构图,模特坐在氛围感清吧的吧台前,眼神微醺迷离,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]在暖光下的质感。采用明亮的暖黄色点光源与背景微弱的白光对比,背景为高度虚化的酒瓶阵列与杯光交错。画面比例为3:4。电影级情绪摄影,50mm镜头,f/1.4大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "复古旱冰场霓虹场景"
|
- name: "日落黄昏街头站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然摆动,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特穿着旱冰鞋在复古旱冰场滑行,姿态灵动,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。霓虹紫粉色调与射灯打在模特身上,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,35mm镜头,f/1.4大光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手插兜不遮挡印花,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特倚靠在城市明亮的天桥栏杆旁,眼神清冷,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花在黄昏下的视觉效果与[材质]的质感与垂感。采用日落前明亮的黄金时刻光线与边缘光,背景为高度虚化的明亮车流与城市剪影。画面比例为3:4。赛博朋克潮流摄影,35mm镜头,f/1.4大光圈,8k分辨率。
|
||||||
|
|
||||||
- name: "地下车库工业风场景"
|
- name: "旱冰场站姿场景"
|
||||||
prompt: |
|
prompt: |
|
||||||
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口。【动作硬约束】模特动作必须确保正面印花100%完整可见,双手必须自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净简洁,不可产生与服装颜色冲突的杂色。全身构图,模特背靠在粗犷的地下车库水泥柱上,眼神冷酷,身体转回正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的冷色聚光灯与微弱的暖色环境光对比,背景为高度虚化的昏暗车库通道与反光地面。画面比例为3:4。赛博朋克潮流摄影,55mm镜头,f/2.0光圈,8k分辨率。
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特穿着旱冰鞋站在复古旱冰场中央,姿态从容,身体正对镜头微侧,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的随性与飘逸。明亮的紫粉色调与射灯打在模特身上,背景为高度虚化的霓虹灯带与溜冰场护栏。画面比例为3:4。复古胶片质感,35mm镜头,f/1.4大光圈,8k分辨率。
|
||||||
|
|
||||||
|
- name: "明亮的现代地下车库站姿场景"
|
||||||
|
prompt: |
|
||||||
|
【最高优先级约束】请严格保留参考图1中模特所穿衣服的完整外观,包括其原有的颜色、印花图案、版型款式、材质纹理与缝线细节,绝对禁止对衣服本身进行任何形式的修改、重新设计、变色或改变图案。仅提取这件[商品名称],将其作为单件上衣穿在一位[模特特征]的身上,模特仅单穿这一件衣服,绝对禁止在外面叠加任何外套、夹克或衬衫。模特下半身穿着与整体风格搭配的长裤、短裤或牛仔裤。【负面约束】禁止改变衣服颜色,禁止遮挡材质纹理,禁止遮挡或破坏印花图案完整性,禁止双手交叉抱胸,禁止手部遮挡胸口,禁止裸露下半身,禁止赤脚。【动作硬约束】模特必须保持规范的站姿,双手自然下垂,绝对不可放在胸口或腰间交叉。【稳定性约束】背景必须保持干净明亮,不可产生昏暗杂色。全身构图,模特背靠在明亮的现代地下车库环氧地坪和纯白水泥柱上,眼神冷酷,身体转回正对镜头,整体呈现[服装风格]的调性,展示衣服正面印花与[材质]面料的耐穿属性。采用顶部的明亮白光聚光灯照明,画面清透无暗角,背景为高度虚化的明亮车库通道与反光地面。画面比例为3:4。赛博朋克潮流摄影,55mm镜头,f/2.0光圈,8k分辨率。
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 2026-08-25 工作日志
|
||||||
|
|
||||||
|
## 全量同步 13 国(追加模式,保留现有数据)
|
||||||
|
- 目标:按"之前的流程"同步所有国家。PG plates 表共 17 个(plate 1=目录不计),实际国家 13 个:
|
||||||
|
US(2,3,4,5)、MX(6)、JP(7)、KR(8)、SA(9)、BR(10)、GB(11)、CA(12)、PL(13)、ES(14)、DE(15)、AU(16)、IT(17)。
|
||||||
|
- 关键风险:旧 `__main__` 用 `clear_first=True`(先 DELETE 两表),全量重跑会清空当前库、冲掉手动修改(JP/GB 尺码、JPTM001 图片+价格)。用户选择"追加9国·保留现有"。
|
||||||
|
- 改动 sync_from_pg.py:
|
||||||
|
1. 新增 `SIZE_SYSTEM` 映射:JP/KR=("亚洲尺码","亚洲尺码亚洲常规"),其余默认 ("尺码","欧美尺码常规");SKU 循环里 `sgroup, stype = SIZE_SYSTEM.get(country, default)` 替换原写死的 `尺码/欧美尺码常规`。
|
||||||
|
2. `__main__` 改为 13 国全部 `clear_first=False`(追加)。已存在的 SPU code / SKU 组合自动跳过,不覆盖手动编辑。
|
||||||
|
- 结果:SPU 93→143(+50),SKU 2025→2875(+850)。13 国全到齐。
|
||||||
|
- 校验:US/MX/JP/GB 数量与重跑前一致(1400/123/244/258);JP=亚洲尺码、GB=欧美尺码、JPTM001-BL01(cdnfe图+price=42) 均保留;KR=亚洲尺码;新国 SPU code 与旧国零重复。
|
||||||
|
- 备份:`spu_sku.db.bak_pre_fullsync`(重跑前含手动修改的整库)。
|
||||||
|
- SA 的 PG categories 4 条但只同步 3 个 SPU(1 条 code 为空被脚本 `c.code IS NOT NULL AND c.code<>''` 过滤,属正常)。
|
||||||
|
|
||||||
|
## 尺码体系约定(最终)
|
||||||
|
- 亚洲市场(JP/KR)→ size_group=亚洲尺码 / size_type=亚洲尺码亚洲常规
|
||||||
|
- 欧美及其它市场(US/MX/GB/CA/PL/ES/DE/AU/BR/IT/SA)→ size_group=欧美尺码 / size_type=欧美尺码常规
|
||||||
|
- 用户 8/25 明确:除 JP/KR 外其余 11 国 size_group 一律"欧美尺码"(含 GB 之前手动改的也符合)。执行 UPDATE(WHERE country NOT IN ('JP','KR')),影响 2579 行,size_type 不动。备份 spu_sku.db.bak_sizegroup。
|
||||||
|
- 注:现在 size_group 仅两种值(亚洲尺码/欧美尺码),口径已统一;sync_from_pg.py 中 SIZE_SYSTEM 默认仍是"尺码",与库内现状不一致,若以后重跑追加新国家需注意让默认对齐"欧美尺码"。
|
||||||
@@ -41,6 +41,13 @@ import json
|
|||||||
PG = dict(host="localhost", port=5432, user="postgres", password="inkreach", dbname="inkreach")
|
PG = dict(host="localhost", port=5432, user="postgres", password="inkreach", dbname="inkreach")
|
||||||
SQLITE = r"C:\Users\Admin\Desktop\test模版\design_agent\pod_trend_agent\db\spu_sku.db"
|
SQLITE = r"C:\Users\Admin\Desktop\test模版\design_agent\pod_trend_agent\db\spu_sku.db"
|
||||||
|
|
||||||
|
# 尺码体系按国家区分:亚洲市场(JP/KR)用亚洲尺码,其余默认 欧美尺码/欧美尺码常规
|
||||||
|
# (库内约定:除 JP/KR 外所有国家 size_group 一律"欧美尺码",与下方默认一致)
|
||||||
|
SIZE_SYSTEM = {
|
||||||
|
"JP": ("亚洲尺码", "亚洲尺码亚洲常规"),
|
||||||
|
"KR": ("亚洲尺码", "亚洲尺码亚洲常规"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def clean(v):
|
def clean(v):
|
||||||
if v is None:
|
if v is None:
|
||||||
@@ -466,9 +473,8 @@ def sync(plate_ids, country, clear_first=True):
|
|||||||
img5 = urls[4] if len(urls) > 4 else None
|
img5 = urls[4] if len(urls) > 4 else None
|
||||||
|
|
||||||
price = price_by_code.get(code)
|
price = price_by_code.get(code)
|
||||||
# 按用户要求:size_group 固定为"尺码",size_type 固定为"欧美尺码常规"
|
# 尺码体系按国家区分:亚洲市场(JP/KR)=亚洲尺码,其余默认 欧美尺码/欧美尺码常规
|
||||||
sgroup = "尺码"
|
sgroup, stype = SIZE_SYSTEM.get(country, ("欧美尺码", "欧美尺码常规"))
|
||||||
stype = "欧美尺码常规"
|
|
||||||
|
|
||||||
for col in color_list:
|
for col in color_list:
|
||||||
raw_code = col["code"]
|
raw_code = col["code"]
|
||||||
@@ -529,9 +535,20 @@ def sync(plate_ids, country, clear_first=True):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 顺序执行:先全量同步美国(重置整库),再依次追加其他国家(保留已有数据)
|
# 追加模式(clear_first=False):保留现有 US/MX/JP/GB 及手动修改,仅补齐其余 9 国
|
||||||
sync((2, 3, 4, 5), "US", clear_first=True)
|
# 任何国家若已存在会被跳过(SPU code / SKU 组合去重),不会覆盖手动编辑
|
||||||
|
sync((2, 3, 4, 5), "US", clear_first=False)
|
||||||
sync((6,), "MX", clear_first=False)
|
sync((6,), "MX", clear_first=False)
|
||||||
sync((7,), "JP", clear_first=False)
|
sync((7,), "JP", clear_first=False)
|
||||||
sync((11,), "GB", clear_first=False)
|
sync((11,), "GB", clear_first=False)
|
||||||
print("\n✅ 同步完成:SPU/SKU 已包含 US + MX + JP + GB 数据")
|
# 新增国家
|
||||||
|
sync((8,), "KR", clear_first=False)
|
||||||
|
sync((9,), "SA", clear_first=False)
|
||||||
|
sync((10,), "BR", clear_first=False)
|
||||||
|
sync((12,), "CA", clear_first=False)
|
||||||
|
sync((13,), "PL", clear_first=False)
|
||||||
|
sync((14,), "ES", clear_first=False)
|
||||||
|
sync((15,), "DE", clear_first=False)
|
||||||
|
sync((16,), "AU", clear_first=False)
|
||||||
|
sync((17,), "IT", clear_first=False)
|
||||||
|
print("\n✅ 同步完成:13 国数据已包含(US/MX/JP/GB 保留,KR/SA/BR/CA/PL/ES/DE/AU/IT 已追加)")
|
||||||
|
|||||||
@@ -58,35 +58,79 @@ def build_graph():
|
|||||||
return builder.compile()
|
return builder.compile()
|
||||||
|
|
||||||
|
|
||||||
|
def _pinterest_route(state: Dict[str, Any]) -> str:
|
||||||
|
"""图池路由:简报达标 → done;图池还有未消费图片 → analyze(继续分析,不搜索);
|
||||||
|
图池不足 → search(新一轮搜索);轮次耗尽 → done。"""
|
||||||
|
target = int(state.get("pinterest_target") or 0)
|
||||||
|
if target <= 0:
|
||||||
|
target = 1
|
||||||
|
briefs = state.get("briefs") or []
|
||||||
|
rounds = int(state.get("pinterest_rounds") or 0)
|
||||||
|
terms = state.get("pinterest_search_terms") or []
|
||||||
|
pcfg = (state.get("config") or {}).get("pinterest") or {}
|
||||||
|
max_rounds = int(pcfg.get("max_search_rounds") or 0)
|
||||||
|
if max_rounds <= 0:
|
||||||
|
max_rounds = max(target * 2, 5)
|
||||||
|
|
||||||
|
if len(briefs) >= target:
|
||||||
|
print(f"[pinterest_route] 简报已达目标 {len(briefs)}/{target},结束")
|
||||||
|
return "done"
|
||||||
|
|
||||||
|
# 图池还有未消费图片 → 继续分析(不搜索)
|
||||||
|
try:
|
||||||
|
from graph.pinterest import load_image_pool, load_used_images, pool_unused_images
|
||||||
|
pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "")
|
||||||
|
used = load_used_images(str(state.get("output_dir") or ""), state.get("country") or "")
|
||||||
|
unused = pool_unused_images(pool, used)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
unused = []
|
||||||
|
if unused:
|
||||||
|
print(f"[pinterest_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target})")
|
||||||
|
return "analyze"
|
||||||
|
|
||||||
|
# 图池不足 → 搜索
|
||||||
|
if rounds >= max_rounds:
|
||||||
|
print(f"[pinterest_route] 已达最大轮次 {max_rounds},简报 {len(briefs)}/{target},按现有结果继续")
|
||||||
|
return "done"
|
||||||
|
if not terms and rounds > 0:
|
||||||
|
print(f"[pinterest_route] 无可用搜索词,停止搜索(简报 {len(briefs)}/{target})")
|
||||||
|
return "done"
|
||||||
|
print(f"[pinterest_route] 图池不足,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target})")
|
||||||
|
return "search"
|
||||||
|
|
||||||
|
|
||||||
def build_pinterest_graph():
|
def build_pinterest_graph():
|
||||||
"""Pinterest 参考模式图(独立于 Google Trends 采集链路):
|
"""Pinterest 参考模式图(按需搜索循环 + 简报池并发生成):
|
||||||
pinterest_search → pinterest_scrape → pinterest_analyze → compose → product
|
pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze
|
||||||
→ oss_upload → seed_shot → template_export
|
→ [pinterest_route] 简报不足 → 回到 pinterest_search;达标 → pinterest_finalize
|
||||||
|
(排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export
|
||||||
"""
|
"""
|
||||||
from graph.nodes import (
|
from graph.nodes import (
|
||||||
pinterest_analyze_node,
|
pinterest_analyze_node,
|
||||||
pinterest_scrape_node,
|
pinterest_scrape_node,
|
||||||
pinterest_search_node,
|
pinterest_search_node,
|
||||||
)
|
)
|
||||||
|
from graph.nodes.pinterest_finalize_node import pinterest_finalize_node
|
||||||
|
from graph.nodes.pinterest_init_node import pinterest_init_node
|
||||||
|
|
||||||
builder = StateGraph(AgentState)
|
builder = StateGraph(AgentState)
|
||||||
|
builder.add_node("pinterest_init", pinterest_init_node)
|
||||||
builder.add_node("pinterest_search", pinterest_search_node)
|
builder.add_node("pinterest_search", pinterest_search_node)
|
||||||
builder.add_node("pinterest_scrape", pinterest_scrape_node)
|
builder.add_node("pinterest_scrape", pinterest_scrape_node)
|
||||||
builder.add_node("pinterest_analyze", pinterest_analyze_node)
|
builder.add_node("pinterest_analyze", pinterest_analyze_node)
|
||||||
builder.add_node("compose", compose_node)
|
builder.add_node("pinterest_finalize", pinterest_finalize_node)
|
||||||
builder.add_node("product", product_node)
|
|
||||||
builder.add_node("oss_upload", oss_upload_node)
|
|
||||||
builder.add_node("seed_shot", seed_shot_node)
|
|
||||||
builder.add_node("template_export", template_export_node)
|
builder.add_node("template_export", template_export_node)
|
||||||
|
|
||||||
builder.add_edge("__start__", "pinterest_search")
|
builder.add_edge("__start__", "pinterest_init")
|
||||||
|
builder.add_edge("pinterest_init", "pinterest_search")
|
||||||
builder.add_edge("pinterest_search", "pinterest_scrape")
|
builder.add_edge("pinterest_search", "pinterest_scrape")
|
||||||
builder.add_edge("pinterest_scrape", "pinterest_analyze")
|
builder.add_edge("pinterest_scrape", "pinterest_analyze")
|
||||||
builder.add_edge("pinterest_analyze", "compose")
|
builder.add_conditional_edges("pinterest_analyze", _pinterest_route, {
|
||||||
builder.add_edge("compose", "product")
|
"analyze": "pinterest_analyze", # 图池还有未消费图片 → 继续分析(不搜索)
|
||||||
builder.add_edge("product", "oss_upload")
|
"search": "pinterest_search", # 图池不足 → 新一轮搜索
|
||||||
builder.add_edge("oss_upload", "seed_shot")
|
"done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池)
|
||||||
builder.add_edge("seed_shot", "template_export")
|
})
|
||||||
|
builder.add_edge("pinterest_finalize", "template_export")
|
||||||
builder.add_edge("template_export", END)
|
builder.add_edge("template_export", END)
|
||||||
return builder.compile()
|
return builder.compile()
|
||||||
|
|
||||||
@@ -183,5 +227,10 @@ def run_pinterest_ref(
|
|||||||
"stats": {},
|
"stats": {},
|
||||||
"task_timestamp": ts,
|
"task_timestamp": ts,
|
||||||
"oss_seq": 0,
|
"oss_seq": 0,
|
||||||
|
# 按需搜索目标:简报数 = spu_tasks 数量(每款一个设计);无任务时回退 spu_count/1
|
||||||
|
"pinterest_target": len((global_config.get("product") or {}).get("spu_tasks") or [])
|
||||||
|
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1,
|
||||||
|
"pinterest_rounds": 0,
|
||||||
|
"pinterest_attempted": [],
|
||||||
}
|
}
|
||||||
return compiled.invoke(state)
|
return compiled.invoke(state)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ImageBackend(ABC):
|
|||||||
def print(self, prompt: str, base_image: str, out_path: str, negative: str = "",
|
def print(self, prompt: str, base_image: str, out_path: str, negative: str = "",
|
||||||
extra_images: Optional[Sequence[str]] = None, size: str = "") -> str:
|
extra_images: Optional[Sequence[str]] = None, size: str = "") -> str:
|
||||||
"""返回生成的成品图路径。实现内部应处理调用失败/超时并抛异常由调用方兜底。
|
"""返回生成的成品图路径。实现内部应处理调用失败/超时并抛异常由调用方兜底。
|
||||||
size: 显式尺寸覆盖(如 "1504x2000");留空则用后端配置的 size。"""
|
size: 显式尺寸覆盖(如 "1536x2048");留空则用后端配置的 size。"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "") -> str:
|
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "") -> str:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MockImageBackend(ImageBackend):
|
|||||||
self._cfg = cfg or {}
|
self._cfg = cfg or {}
|
||||||
|
|
||||||
def print(self, prompt: str, base_image: str, out_path: str, negative: str = "",
|
def print(self, prompt: str, base_image: str, out_path: str, negative: str = "",
|
||||||
extra_images=None, size: str = "") -> str:
|
extra_images=None, size: str = "", seed: int = None) -> str:
|
||||||
size_px = (1024, 1024)
|
size_px = (1024, 1024)
|
||||||
try:
|
try:
|
||||||
with Image.open(base_image) as im:
|
with Image.open(base_image) as im:
|
||||||
@@ -45,7 +45,8 @@ class MockImageBackend(ImageBackend):
|
|||||||
img.save(out_path)
|
img.save(out_path)
|
||||||
return out_path
|
return out_path
|
||||||
|
|
||||||
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "") -> str:
|
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "",
|
||||||
|
seed: int = None) -> str:
|
||||||
"""纯文生图(mock):白底 + 文字标注,模拟纯印花设计稿。"""
|
"""纯文生图(mock):白底 + 文字标注,模拟纯印花设计稿。"""
|
||||||
size_px = (1024, 1024)
|
size_px = (1024, 1024)
|
||||||
if size:
|
if size:
|
||||||
|
|||||||
@@ -137,13 +137,14 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
return str(self._cfg.get("base_url") or "https://api.openai.com/v1").rstrip("/")
|
return str(self._cfg.get("base_url") or "https://api.openai.com/v1").rstrip("/")
|
||||||
|
|
||||||
def print(self, prompt: str, base_image: str, out_path: str, negative: str = "",
|
def print(self, prompt: str, base_image: str, out_path: str, negative: str = "",
|
||||||
extra_images=None, size: str = "") -> str:
|
extra_images=None, size: str = "", seed: Optional[int] = None) -> str:
|
||||||
"""img2img 编辑:参考图 base_image(+ 可选 extra_images 多参考图)按 prompt 生成新图。
|
"""img2img 编辑:参考图 base_image(+ 可选 extra_images 多参考图)按 prompt 生成新图。
|
||||||
|
|
||||||
- 平铺服装图:base_image=平铺衣服底图(图3),extra_images=[印花设计稿(图2)]
|
- 平铺服装图:base_image=平铺衣服底图(图3),extra_images=[印花设计稿(图2)]
|
||||||
- 三图模特合成:base_image=模特图(图1),extra_images=[印花设计稿(图2), 平铺底图(图3)]
|
- 三图模特合成:base_image=模特图(图1),extra_images=[印花设计稿(图2), 平铺底图(图3)]
|
||||||
提交顺序即图1→图2→图3,与提示词中的图片角色一一对应。
|
提交顺序即图1→图2→图3,与提示词中的图片角色一一对应。
|
||||||
size: 显式尺寸覆盖(如 "1504x2000");留空用配置 size(默认 1024x1024)。
|
size: 显式尺寸覆盖(如 "1536x2048");留空用配置 size(默认 1024x1024)。
|
||||||
|
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
|
||||||
"""
|
"""
|
||||||
cfg = self._cfg
|
cfg = self._cfg
|
||||||
api_key = cfg.get("api_key", "")
|
api_key = cfg.get("api_key", "")
|
||||||
@@ -172,6 +173,8 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
"model": model,
|
"model": model,
|
||||||
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
|
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
|
||||||
}
|
}
|
||||||
|
if seed is not None:
|
||||||
|
data["seed"] = seed
|
||||||
# 提交重试:异步路径不稳定 → 失败重试同步提交(最多 3 次);
|
# 提交重试:异步路径不稳定 → 失败重试同步提交(最多 3 次);
|
||||||
# 内容政策拦截(content_policy_violation)多为网关误判 → 等待后重试
|
# 内容政策拦截(content_policy_violation)多为网关误判 → 等待后重试
|
||||||
last_err: Optional[str] = None
|
last_err: Optional[str] = None
|
||||||
@@ -195,9 +198,11 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
print(f"[img] 第 {attempt + 1} 次提交异步失败,重试同步提交: {e}")
|
print(f"[img] 第 {attempt + 1} 次提交异步失败,重试同步提交: {e}")
|
||||||
raise RuntimeError(f"图像合成多次提交均失败: {last_err}")
|
raise RuntimeError(f"图像合成多次提交均失败: {last_err}")
|
||||||
|
|
||||||
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "") -> str:
|
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "",
|
||||||
|
seed: Optional[int] = None) -> str:
|
||||||
"""纯文生图:生成白底纯印花设计稿(standalone pure print design)。
|
"""纯文生图:生成白底纯印花设计稿(standalone pure print design)。
|
||||||
size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。
|
size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。
|
||||||
|
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
|
||||||
background: 配置 compose.background="transparent" 时传 background 参数 → 透明背景 PNG
|
background: 配置 compose.background="transparent" 时传 background 参数 → 透明背景 PNG
|
||||||
(gpt-image-1/2 等模型支持;网关不支持该参数时会被忽略或由网关兜底)。"""
|
(gpt-image-1/2 等模型支持;网关不支持该参数时会被忽略或由网关兜底)。"""
|
||||||
cfg = self._cfg
|
cfg = self._cfg
|
||||||
@@ -217,6 +222,8 @@ class OpenAIImageBackend(ImageBackend):
|
|||||||
"response_format": "b64_json",
|
"response_format": "b64_json",
|
||||||
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
|
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
|
||||||
}
|
}
|
||||||
|
if seed is not None:
|
||||||
|
data["seed"] = seed
|
||||||
bg = str(cfg.get("background") or "").strip()
|
bg = str(cfg.get("background") or "").strip()
|
||||||
if bg:
|
if bg:
|
||||||
data["background"] = bg # 如 "transparent"(透明背景 PNG)
|
data["background"] = bg # 如 "transparent"(透明背景 PNG)
|
||||||
|
|||||||
@@ -156,16 +156,18 @@ class MockBackend:
|
|||||||
random.shuffle(pool)
|
random.shuffle(pool)
|
||||||
terms = pool[:count]
|
terms = pool[:count]
|
||||||
# 不足时用「种子词 + 风格词」组合补足(视觉导向,避免与已用重复)
|
# 不足时用「种子词 + 风格词」组合补足(视觉导向,避免与已用重复)
|
||||||
style_tail = ["aesthetic", "style", "inspiration", "design", "vibe", "art"]
|
style_tail = ["t-shirt design", "graphic tee", "print art", "vintage tee", "flat design"]
|
||||||
i = 0
|
i = 0
|
||||||
while len(terms) < count and pool:
|
while len(terms) < count and pool:
|
||||||
combo = f"{pool[i % len(pool)]} {style_tail[(i // len(pool)) % len(style_tail)]}"
|
combo = f"{pool[i % len(pool)]} {style_tail[(i // len(pool)) % len(style_tail)]}"
|
||||||
if combo.lower() not in used and combo not in terms:
|
if combo.lower() not in used and combo not in terms:
|
||||||
terms.append(combo)
|
terms.append(combo)
|
||||||
i += 1
|
i += 1
|
||||||
|
# 自动追加 " t-shirt design":让 Pinterest 返回真正的 T 恤印花图(更适合作印花设计参考)
|
||||||
|
terms = [f"{t} t-shirt design" if "t-shirt design" not in t.lower() else t for t in terms]
|
||||||
return {"search_terms": terms}
|
return {"search_terms": terms}
|
||||||
|
|
||||||
def analyze_pinterest_images(self, image_paths, term="", country=""):
|
def analyze_pinterest_images(self, image_paths, term="", country="", on_400=None):
|
||||||
"""规则生成设计简报(零 API 成本):按搜索词启发式推导风格/配色/构图。"""
|
"""规则生成设计简报(零 API 成本):按搜索词启发式推导风格/配色/构图。"""
|
||||||
from ..classify import classify, prompt_suggestion
|
from ..classify import classify, prompt_suggestion
|
||||||
cat = classify(term)
|
cat = classify(term)
|
||||||
@@ -184,6 +186,8 @@ class MockBackend:
|
|||||||
"color_palette": palette,
|
"color_palette": palette,
|
||||||
"composition": composition,
|
"composition": composition,
|
||||||
"negative_prompt": negative,
|
"negative_prompt": negative,
|
||||||
|
"image_prompt": (f"{motif}, {art_style}, {palette}, {composition}, "
|
||||||
|
f"original {art_style} t-shirt print design"),
|
||||||
# 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应)
|
# 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应)
|
||||||
"ref_images": [str(paths[i])] if i < len(paths) else [],
|
"ref_images": [str(paths[i])] if i < len(paths) else [],
|
||||||
"source": "pinterest",
|
"source": "pinterest",
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ from typing import Any, Dict, List
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
# 模型调用一律直连:用户常开 VPN(系统代理),LLM 网关多为国内/自建,走代理会被拦截或变慢。
|
# 模型调用一律直连:用户常开 VPN(系统代理),LLM 网关多为国内/自建,走代理会被拦截或变慢。
|
||||||
# 环境变量级 NO_PROXY 双保险(requests/urllib3 均读取),Google 采集(pytrends)不受影响。
|
# 仅请求级 proxies=NO_PROXY 直连,不设置进程级 NO_PROXY 环境变量(避免影响 Google Trends 等外部采集)。
|
||||||
os.environ.setdefault("NO_PROXY", "*")
|
|
||||||
os.environ.setdefault("no_proxy", "*")
|
|
||||||
from .base import LLMBackend
|
from .base import LLMBackend
|
||||||
from graph.paths import runtime_root
|
from graph.paths import runtime_root
|
||||||
|
|
||||||
@@ -107,6 +105,7 @@ Rules:
|
|||||||
# 模板字典按编号存放;TITLE_TEMPLATE_ROUTE 按国家路由到模板编号。
|
# 模板字典按编号存放;TITLE_TEMPLATE_ROUTE 按国家路由到模板编号。
|
||||||
# 模板 1:英语市场(US/GB/AU/MX)→ en_title + cn_title
|
# 模板 1:英语市场(US/GB/AU/MX)→ en_title + cn_title
|
||||||
# 模板 2:日本市场(JP)→ en_title + cn_title + ja_title
|
# 模板 2:日本市场(JP)→ en_title + cn_title + ja_title
|
||||||
|
# 模板 3:西班牙市场(ES)→ es_title + cn_title
|
||||||
TITLE_TEMPLATES: Dict[str, str] = {
|
TITLE_TEMPLATES: Dict[str, str] = {
|
||||||
"1": '''# Role
|
"1": '''# Role
|
||||||
你是一位资深的跨境服装运营专家,精通英语电商的SEO标题逻辑。你的任务是通过分析服装图片,生成高权重的英语-中文商品标题。
|
你是一位资深的跨境服装运营专家,精通英语电商的SEO标题逻辑。你的任务是通过分析服装图片,生成高权重的英语-中文商品标题。
|
||||||
@@ -151,15 +150,36 @@ TITLE_TEMPLATES: Dict[str, str] = {
|
|||||||
|
|
||||||
- **Output**: 必须严格返回 JSON 格式,不要包含 Markdown 代码块标记,格式如下:
|
- **Output**: 必须严格返回 JSON 格式,不要包含 Markdown 代码块标记,格式如下:
|
||||||
{"en_title": "Title in English", "cn_title": "中文标题", "ja_title": "日本語タイトル"}''',
|
{"en_title": "Title in English", "cn_title": "中文标题", "ja_title": "日本語タイトル"}''',
|
||||||
|
"3": '''# Role
|
||||||
|
你是一位资深的跨境服装运营专家,精通西班牙语电商(Amazon ES, MercadoLibre)的SEO标题逻辑。你的任务是通过分析服装图片,生成高权重的西班牙语-中文商品标题。
|
||||||
|
|
||||||
|
# Task
|
||||||
|
请深度分析图片中的服装特征(品类、风格、材质、剪裁、细节、受众),生成符合西语电商搜索逻辑的中西文标题。
|
||||||
|
|
||||||
|
# 当前时间(标题须贴合当下,季节/年份词以此为准)
|
||||||
|
- **Current time**: {year}-{month}({season}),标题中的年份/季节等时效词必须使用以上时间。
|
||||||
|
|
||||||
|
# Analysis Focus (视觉分析重点)
|
||||||
|
- 品类识别:准确判断西班牙语核心词(如 Vestido, Blusa, Sudadera)和中文核心词(如 连衣裙, 卫衣)。
|
||||||
|
- 风格定位:判断风格流派(如 Boho, Vintage, Minimalista / 法式, 复古, 极简)。
|
||||||
|
- 设计细节:提取领型、袖型、裙长等(如 Escote en V, Manga abullonada / V领, 阔袖)。
|
||||||
|
- 适用场景:推断穿着场景(如 Playa, Oficina, Fiesta / 度假, 通勤, 约会)。
|
||||||
|
|
||||||
|
# Constraints (生成规则)
|
||||||
|
- Spanish Title: 遵循 Amazon ES/MercadoLibre 风格,核心词前置,包含材质、风格、场景等长尾词,符合西语搜索习惯。
|
||||||
|
- Chinese Title: 遵循淘宝/1688风格,关键词权重递减,包含年份/季节+风格+核心词+卖点+人群。
|
||||||
|
- Output: 必须严格返回 JSON 格式,不要包含 Markdown 代码块标记,格式如下:
|
||||||
|
{"es_title": "Título en español", "cn_title": "中文标题"}''',
|
||||||
}
|
}
|
||||||
|
|
||||||
# 国家 → 标题模板编号(JP 路由到模板 2,其余默认模板 1;后续可按国家新增模板 3...)
|
# 国家 → 标题模板编号(JP 路由到模板 2,ES 路由到模板 3,其余默认模板 1;后续可按国家新增模板)
|
||||||
TITLE_TEMPLATE_ROUTE: Dict[str, str] = {
|
TITLE_TEMPLATE_ROUTE: Dict[str, str] = {
|
||||||
"US": "1",
|
"US": "1",
|
||||||
"GB": "1",
|
"GB": "1",
|
||||||
"JP": "2",
|
"JP": "2",
|
||||||
"AU": "1",
|
"AU": "1",
|
||||||
"MX": "1",
|
"MX": "1",
|
||||||
|
"ES": "3",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -234,11 +254,19 @@ def build_user_prompt(country, topics, aesthetic_hint):
|
|||||||
|
|
||||||
|
|
||||||
# —— Pinterest 参考模式:搜索词生成(json_schema 结构化 + 动态注入已用词防重复)——
|
# —— Pinterest 参考模式:搜索词生成(json_schema 结构化 + 动态注入已用词防重复)——
|
||||||
PINTEREST_TERM_SYSTEM_PROMPT = """You are a Pinterest search-term generator for print-on-demand (POD) T-shirt design.
|
PINTEREST_TERM_SYSTEM_PROMPT = """You are a Pinterest search-term generator for print-on-demand (POD) SHORT-SLEEVE T-SHIRT print design.
|
||||||
You turn seed words into diverse, visual, Pinterest-friendly search terms that will be used to scrape inspiration images.
|
You turn seed words into diverse, visual, Pinterest-friendly search terms that will be used to scrape inspiration images
|
||||||
|
that are DIRECTLY usable as reference for a t-shirt print design.
|
||||||
|
|
||||||
RULES:
|
RULES:
|
||||||
- Generate EXACTLY the requested number of search terms.
|
- Generate EXACTLY the requested number of search terms (usually 1 per call).
|
||||||
|
- Every term MUST be a "t-shirt design" style query: think of it as if the user typed "<concept> t-shirt design" on
|
||||||
|
Pinterest, so the scraped images are actual t-shirt graphics / flat print artworks, NOT lifestyle photos, scenery,
|
||||||
|
architecture, food plates, or anything that cannot become a clean chest print.
|
||||||
|
- Terms MUST be suitable for a SHORT-SLEEVE T-SHIRT PRINT: a flat, graphic, print-ready concept (illustration, mascot,
|
||||||
|
emblem, pattern, typography, slogan) that works as a chest print between about 15x18 cm and 26x32 cm.
|
||||||
|
- Prefer a clear central subject with a strong silhouette and balanced composition that reads well as a standalone print.
|
||||||
|
- AVOID terms that lead to full-scene photos, landscapes, architecture, food plates, or anything that cannot become a clean t-shirt print.
|
||||||
- Terms must be VISUAL / AESTHETIC concepts (style, motif, scene, color) suitable as T-shirt print inspiration.
|
- Terms must be VISUAL / AESTHETIC concepts (style, motif, scene, color) suitable as T-shirt print inspiration.
|
||||||
- Terms must be DIVERSE and NON-OVERLAPPING: never repeat a concept, never give near-synonyms of each other.
|
- Terms must be DIVERSE and NON-OVERLAPPING: never repeat a concept, never give near-synonyms of each other.
|
||||||
- DO NOT repeat or closely paraphrase ANY of the "already used terms" provided in the user message.
|
- DO NOT repeat or closely paraphrase ANY of the "already used terms" provided in the user message.
|
||||||
@@ -257,7 +285,7 @@ PINTEREST_TERM_SCHEMA = {
|
|||||||
"search_terms": {
|
"search_terms": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
"description": "Diverse, non-overlapping Pinterest search terms for T-shirt design inspiration",
|
"description": "Diverse, non-overlapping Pinterest search terms for short-sleeve t-shirt print design inspiration",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["search_terms"],
|
"required": ["search_terms"],
|
||||||
@@ -267,10 +295,10 @@ PINTEREST_TERM_SCHEMA = {
|
|||||||
|
|
||||||
|
|
||||||
def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
|
def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
|
||||||
"""动态注入:种子词(灵感)+ 已用搜索词(禁止重复)+ 数量要求。"""
|
"""动态注入:种子词(灵感)+ 已用搜索词(禁止重复)+ 数量要求(按需每次 1 个)。"""
|
||||||
seeds = context.get("seeds", []) or []
|
seeds = context.get("seeds", []) or []
|
||||||
used = context.get("used_terms", []) or []
|
used = context.get("used_terms", []) or []
|
||||||
count = int(context.get("count", 10))
|
count = int(context.get("count", 1))
|
||||||
lines = [
|
lines = [
|
||||||
f"Country: {context.get('country', '')}",
|
f"Country: {context.get('country', '')}",
|
||||||
f"Seed words (inspiration, may combine or extend): {', '.join(seeds)}",
|
f"Seed words (inspiration, may combine or extend): {', '.join(seeds)}",
|
||||||
@@ -278,12 +306,18 @@ def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
|
|||||||
f"Already used terms — DO NOT repeat or paraphrase ANY of these: "
|
f"Already used terms — DO NOT repeat or paraphrase ANY of these: "
|
||||||
f"{', '.join(used) if used else '(none yet)'}",
|
f"{', '.join(used) if used else '(none yet)'}",
|
||||||
"",
|
"",
|
||||||
f"Generate {count} new, diverse, non-overlapping Pinterest search terms.",
|
f"Generate {count} new, diverse, non-overlapping Pinterest search term(s) "
|
||||||
|
f"that are suitable for a SHORT-SLEEVE T-SHIRT PRINT design "
|
||||||
|
f"(flat, graphic, print-ready motif that works as a chest print). "
|
||||||
|
f"Each term should read like \"<concept> t-shirt design\" so Pinterest returns "
|
||||||
|
f"actual t-shirt graphics / flat print artwork as reference.",
|
||||||
]
|
]
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
# —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)——
|
# —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)——
|
||||||
|
# image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接),
|
||||||
|
# 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。
|
||||||
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD (print-on-demand) T-shirt design analyst.
|
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD (print-on-demand) T-shirt design analyst.
|
||||||
You receive Pinterest reference images for one search term. For each image, extract the VISUAL CONCEPT
|
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
|
(style, mood, motif, color palette, composition) that makes it appealing, then produce an ORIGINAL
|
||||||
@@ -301,9 +335,14 @@ RULES:
|
|||||||
- composition: English layout (e.g. "centered emblem with balanced negative space").
|
- composition: English layout (e.g. "centered emblem with balanced negative space").
|
||||||
- concept: Chinese, one sentence describing the design idea.
|
- concept: Chinese, one sentence describing the design idea.
|
||||||
- negative_prompt: what to avoid (real people, likeness, characters, logos, text).
|
- 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.
|
||||||
|
|
||||||
Return JSON with the field "designs" (array of objects with keys:
|
Return JSON with the field "designs" (array of objects with keys:
|
||||||
motif, art_style, color_palette, composition, concept, negative_prompt)."""
|
motif, art_style, color_palette, composition, concept, negative_prompt, image_prompt)."""
|
||||||
|
|
||||||
PINTEREST_ANALYZE_SCHEMA = {
|
PINTEREST_ANALYZE_SCHEMA = {
|
||||||
"name": "pinterest_design_briefs",
|
"name": "pinterest_design_briefs",
|
||||||
@@ -315,15 +354,17 @@ PINTEREST_ANALYZE_SCHEMA = {
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"image_index": {"type": "integer"},
|
||||||
"motif": {"type": "string"},
|
"motif": {"type": "string"},
|
||||||
"art_style": {"type": "string"},
|
"art_style": {"type": "string"},
|
||||||
"color_palette": {"type": "string"},
|
"color_palette": {"type": "string"},
|
||||||
"composition": {"type": "string"},
|
"composition": {"type": "string"},
|
||||||
"concept": {"type": "string"},
|
"concept": {"type": "string"},
|
||||||
"negative_prompt": {"type": "string"},
|
"negative_prompt": {"type": "string"},
|
||||||
|
"image_prompt": {"type": "string"},
|
||||||
},
|
},
|
||||||
"required": ["motif", "art_style", "color_palette", "composition",
|
"required": ["image_index", "motif", "art_style", "color_palette",
|
||||||
"concept", "negative_prompt"],
|
"composition", "concept", "negative_prompt", "image_prompt"],
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -341,7 +382,11 @@ def build_pinterest_analyze_user_prompt(term: str, country: str, image_count: in
|
|||||||
f"Reference images attached: {image_count} images.\n\n"
|
f"Reference images attached: {image_count} images.\n\n"
|
||||||
f"Analyze the attached images and produce {image_count} ORIGINAL design briefs "
|
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"(one per image), each capturing the visual vibe as an original T-shirt print design. "
|
||||||
f"Do NOT copy the images."
|
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."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -419,9 +464,31 @@ def _extract_json(text):
|
|||||||
try:
|
try:
|
||||||
return json.loads(text)
|
return json.loads(text)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
m = re.search(r"\{.*\}", text, re.S)
|
# 找第一个 { 到与之平衡的 },逐字符跳过字符串内的花括号,避免贪婪匹配截断 JSON
|
||||||
if m:
|
start = text.find("{")
|
||||||
return json.loads(m.group(0))
|
if start == -1:
|
||||||
|
raise
|
||||||
|
depth = 0
|
||||||
|
in_str = False
|
||||||
|
esc = False
|
||||||
|
for i in range(start, len(text)):
|
||||||
|
ch = text[i]
|
||||||
|
if in_str:
|
||||||
|
if esc:
|
||||||
|
esc = False
|
||||||
|
elif ch == "\\":
|
||||||
|
esc = True
|
||||||
|
elif ch == '"':
|
||||||
|
in_str = False
|
||||||
|
else:
|
||||||
|
if ch == '"':
|
||||||
|
in_str = True
|
||||||
|
elif ch == "{":
|
||||||
|
depth += 1
|
||||||
|
elif ch == "}":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
return json.loads(text[start:i + 1])
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -512,13 +579,17 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
raw = _retry(lambda: call_openai_compatible_structured(cfg, messages, PINTEREST_TERM_SCHEMA, timeout=120))
|
raw = _retry(lambda: call_openai_compatible_structured(cfg, messages, PINTEREST_TERM_SCHEMA, timeout=120))
|
||||||
parsed = _extract_json(raw)
|
parsed = _extract_json(raw)
|
||||||
terms = [str(x).strip() for x in (parsed.get("search_terms", []) or []) if str(x).strip()]
|
terms = [str(x).strip() for x in (parsed.get("search_terms", []) or []) if str(x).strip()]
|
||||||
|
# 自动追加 " t-shirt design":让 Pinterest 返回真正的 T 恤印花图(更适合作印花设计参考)
|
||||||
|
terms = [f"{t} t-shirt design" if "t-shirt design" not in t.lower() else t for t in terms]
|
||||||
return {"search_terms": terms}
|
return {"search_terms": terms}
|
||||||
|
|
||||||
def analyze_pinterest_images(self, image_paths: List[str], term: str, country: str = "") -> List[Dict[str, Any]]:
|
def analyze_pinterest_images(self, image_paths: List[str], term: str, country: str = "",
|
||||||
|
on_400=None) -> List[Dict[str, Any]]:
|
||||||
"""多模态分析 Pinterest 图片 → 原创设计简报列表。
|
"""多模态分析 Pinterest 图片 → 原创设计简报列表。
|
||||||
|
|
||||||
图片输入不被模型支持(纯文本模型 400)时自动降级为纯文本分析(仅用搜索词)。
|
图片输入不被模型支持(纯文本模型 400)时自动降级为纯文本分析(仅用搜索词)。
|
||||||
失败返回 [],由节点兜底(回退 mock 规则简报)。
|
失败返回 [],由节点兜底(回退 mock 规则简报)。
|
||||||
|
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
|
||||||
"""
|
"""
|
||||||
cfg = self._cfg
|
cfg = self._cfg
|
||||||
api_key = cfg.get("api_key", "")
|
api_key = cfg.get("api_key", "")
|
||||||
@@ -542,6 +613,16 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[pinterest_analyze] 图片读取失败 {p}: {e}")
|
print(f"[pinterest_analyze] 图片读取失败 {p}: {e}")
|
||||||
|
|
||||||
|
def _notify_400(exc) -> None:
|
||||||
|
if on_400 is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from graph.pinterest import is_400_content_image
|
||||||
|
if is_400_content_image(exc):
|
||||||
|
on_400()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
def _call(use_images: bool) -> str:
|
def _call(use_images: bool) -> str:
|
||||||
user_content: List[Any] = [
|
user_content: List[Any] = [
|
||||||
{"type": "text", "text": build_pinterest_analyze_user_prompt(term, country, len(data_uris))},
|
{"type": "text", "text": build_pinterest_analyze_user_prompt(term, country, len(data_uris))},
|
||||||
@@ -568,7 +649,8 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
resp = requests.post(url, json=payload, headers=headers, timeout=180, proxies=NO_PROXY)
|
resp = requests.post(url, json=payload, headers=headers, timeout=180, proxies=NO_PROXY)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return str(resp.json()["choices"][0]["message"].get("content") or "")
|
return str(resp.json()["choices"][0]["message"].get("content") or "")
|
||||||
except Exception: # noqa: BLE001 兼容厂商不支持 json_schema
|
except Exception as e: # noqa: BLE001 兼容厂商不支持 json_schema
|
||||||
|
_notify_400(e)
|
||||||
payload["response_format"] = {"type": "json_object"}
|
payload["response_format"] = {"type": "json_object"}
|
||||||
resp = requests.post(url, json=payload, headers=headers, timeout=180, proxies=NO_PROXY)
|
resp = requests.post(url, json=payload, headers=headers, timeout=180, proxies=NO_PROXY)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
@@ -611,21 +693,20 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
"color_palette": str(d.get("color_palette", "")).strip(),
|
"color_palette": str(d.get("color_palette", "")).strip(),
|
||||||
"composition": str(d.get("composition", "")).strip(),
|
"composition": str(d.get("composition", "")).strip(),
|
||||||
"negative_prompt": str(d.get("negative_prompt", "")).strip(),
|
"negative_prompt": str(d.get("negative_prompt", "")).strip(),
|
||||||
|
"image_prompt": str(d.get("image_prompt", "")).strip(),
|
||||||
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)
|
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)
|
||||||
"ref_images": [str(image_paths[i])] if i < len(image_paths) else [],
|
"ref_images": [str(image_paths[i])] if i < len(image_paths) else [],
|
||||||
"source": "pinterest",
|
"source": "pinterest",
|
||||||
})
|
})
|
||||||
return designs
|
return designs
|
||||||
|
|
||||||
def generate_title(self, image_path: str, system_prompt: str = "", country: str = "",
|
def generate_title(self, image_path: str, system_prompt: str = "", country: str = "") -> Dict[str, Any]:
|
||||||
fallback_text: str = "") -> Dict[str, Any]:
|
|
||||||
"""多模态标题生成;图片输入不被模型支持(如 qwen 纯文本模型 400)时,
|
|
||||||
自动降级为纯文本生成(fallback_text 为商品描述/热点主题)。"""
|
|
||||||
"""多模态:分析服装图片,生成商品标题(按国家路由模板)。
|
"""多模态:分析服装图片,生成商品标题(按国家路由模板)。
|
||||||
|
|
||||||
系统提示词:显式传入优先;否则按 country 经 TITLE_TEMPLATE_ROUTE 路由到对应模板。
|
系统提示词:显式传入优先;否则按 country 经 TITLE_TEMPLATE_ROUTE 路由到对应模板。
|
||||||
模板 1(US/GB/AU/MX)返回 {"en_title","cn_title"};
|
模板 1(US/GB/AU/MX)返回 {"en_title","cn_title"};
|
||||||
模板 2(JP)额外返回 {"ja_title"}。
|
模板 2(JP)额外返回 {"ja_title"};
|
||||||
|
模板 3(ES)返回 {"es_title","cn_title"}。
|
||||||
无 key/调用失败返回 {}(调用方兜底不中断)。
|
无 key/调用失败返回 {}(调用方兜底不中断)。
|
||||||
"""
|
"""
|
||||||
cfg = self._cfg
|
cfg = self._cfg
|
||||||
@@ -678,6 +759,7 @@ class OpenAICompatBackend(LLMBackend):
|
|||||||
"en_title": str(parsed.get("en_title", "")).strip(),
|
"en_title": str(parsed.get("en_title", "")).strip(),
|
||||||
"cn_title": str(parsed.get("cn_title", "")).strip(),
|
"cn_title": str(parsed.get("cn_title", "")).strip(),
|
||||||
"ja_title": str(parsed.get("ja_title", "")).strip(),
|
"ja_title": str(parsed.get("ja_title", "")).strip(),
|
||||||
|
"es_title": str(parsed.get("es_title", "")).strip(),
|
||||||
}
|
}
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[titles] 标题生成失败: {e}")
|
print(f"[titles] 标题生成失败: {e}")
|
||||||
|
|||||||
@@ -10,13 +10,25 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from graph.validate import with_fallback
|
from graph.validate import with_fallback
|
||||||
|
|
||||||
RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}
|
RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_400(on_400, exc) -> None:
|
||||||
|
"""HTTP 400(且含「内容/图片」)时触发 on_400 回调(供调用方累计放弃计数)。"""
|
||||||
|
if on_400 is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from graph.pinterest import is_400_content_image
|
||||||
|
if is_400_content_image(exc):
|
||||||
|
on_400()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
|
def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
|
||||||
lines = [
|
lines = [
|
||||||
"# POD 印花设计简报(LLM 合规筛选 + 生图提示词)",
|
"# POD 印花设计简报(LLM 合规筛选 + 生图提示词)",
|
||||||
@@ -96,6 +108,88 @@ def _build_report_md(state: Dict[str, Any]) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
size: str = "1024x1024") -> Optional[str]:
|
||||||
|
"""生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。
|
||||||
|
|
||||||
|
out_stem: 输出文件名主干(不含扩展名),最终文件 = {out_stem}_design.png。
|
||||||
|
货号模式传 img_code(如 DG000)→ designs/DG000_design.png;
|
||||||
|
旧 compose 模式传 {country}_{idx:02d}(如 JP_01)→ designs/JP_01_design.png。
|
||||||
|
Pinterest 参考模式:简报带 ref_images(爬取图)→ 用 ib.print() 图生图,
|
||||||
|
把爬取图 + 多模态分析简报(已封装进 image_prompt)一起发给生图模型;
|
||||||
|
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
|
||||||
|
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
|
||||||
|
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
|
||||||
|
img_prompt = sanitize_image_prompt(brief.get("image_prompt", ""))
|
||||||
|
img_prompt = ensure_rebrand_hint(brief, img_prompt) # review → 原创化魔改引导
|
||||||
|
out_path = str(design_dir / f"{out_stem}_design.png")
|
||||||
|
ref_images = [str(p) for p in (brief.get("ref_images") or []) if str(p)]
|
||||||
|
if ref_images and hasattr(ib, "print"):
|
||||||
|
try:
|
||||||
|
# 图生图:以爬取图为参考,按分析简报生成原创设计(不复制原图)
|
||||||
|
ref_prompt = img_prompt + (
|
||||||
|
" Create an ORIGINAL, non-copying flat print design inspired ONLY by "
|
||||||
|
"the reference image's style and mood. Do NOT reproduce the reference "
|
||||||
|
"image, its characters, logos, or any text.")
|
||||||
|
out_path = ib.print(
|
||||||
|
ref_prompt, ref_images[0], out_path,
|
||||||
|
brief.get("composite_negative", ""),
|
||||||
|
extra_images=ref_images[1:] or None,
|
||||||
|
size=size, seed=seed) # 设计稿尺寸按 config compose.design_size
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}")
|
||||||
|
_notify_400(on_400, e)
|
||||||
|
out_path = ib.generate(
|
||||||
|
img_prompt, str(design_dir / f"{out_stem}_design.png"),
|
||||||
|
brief.get("composite_negative", ""), size=size, seed=seed)
|
||||||
|
else:
|
||||||
|
out_path = ib.generate(
|
||||||
|
img_prompt, str(design_dir / f"{out_stem}_design.png"),
|
||||||
|
brief.get("composite_negative", ""), size=size, seed=seed)
|
||||||
|
# 全局 MD5 去重:生成了设计后,把 MD5 加入全局过滤(对所有国家生效);
|
||||||
|
# 已存在的重复设计 → 跳过(不用于产品),避免跨国家重复使用同一设计
|
||||||
|
from graph.pinterest import design_md5_ok
|
||||||
|
if not design_md5_ok(out_path):
|
||||||
|
print(f"[compose] 设计稿 MD5 全局重复,跳过(不用于产品): {out_path}")
|
||||||
|
return None
|
||||||
|
return out_path
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
_notify_400(on_400, e)
|
||||||
|
if errors is not None:
|
||||||
|
errors.append({"node": "compose", "type": type(e).__name__,
|
||||||
|
"message": f"设计稿生成失败 {brief.get('topic','')}: {e}", "trace": ""})
|
||||||
|
print(f"[compose] 设计稿生成失败 {brief.get('topic', '')}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def write_compose_reports(state: Dict[str, Any], briefs: List[Dict[str, Any]]) -> None:
|
||||||
|
"""写 compose 阶段简报报告(design_briefs / composite_prompts / report.md)。
|
||||||
|
|
||||||
|
Pinterest 并发生成模式下 compose_node 不再整体执行,由收尾节点调用本函数补写报告。
|
||||||
|
"""
|
||||||
|
output_dir = Path(state["output_dir"])
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
cache_dir = Path(state.get("cache_dir") or output_dir)
|
||||||
|
generated_at = time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
(cache_dir / "design_briefs.json").write_text(
|
||||||
|
json.dumps({"generated_at": generated_at, "total": len(briefs), "design_briefs": briefs},
|
||||||
|
ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
(cache_dir / "design_briefs.md").write_text(
|
||||||
|
_build_briefs_md(briefs, generated_at), encoding="utf-8")
|
||||||
|
(cache_dir / "composite_prompts.json").write_text(
|
||||||
|
json.dumps({"generated_at": generated_at, "total": len(briefs), "composite_prompts": briefs},
|
||||||
|
ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
(cache_dir / "composite_prompts.md").write_text(
|
||||||
|
_build_composite_md(briefs), encoding="utf-8")
|
||||||
|
(output_dir / "report.md").write_text(_build_report_md(state), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
@with_fallback("compose")
|
@with_fallback("compose")
|
||||||
def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
briefs: List[Dict[str, Any]] = state.get("briefs") or []
|
briefs: List[Dict[str, Any]] = state.get("briefs") or []
|
||||||
@@ -105,26 +199,8 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
config = state["config"]
|
config = state["config"]
|
||||||
country = state.get("country", "")
|
country = state.get("country", "")
|
||||||
|
|
||||||
generated_at = time.strftime("%Y-%m-%dT%H:%M:%S")
|
# 1-4) 简报报告(design_briefs / composite_prompts / report.md)
|
||||||
|
write_compose_reports(state, briefs)
|
||||||
# 1) design_briefs.json(缓存 → 根目录,不进时间戳任务文件夹)
|
|
||||||
(cache_dir / "design_briefs.json").write_text(
|
|
||||||
json.dumps({"generated_at": generated_at, "total": len(briefs), "design_briefs": briefs},
|
|
||||||
ensure_ascii=False, indent=2), encoding="utf-8")
|
|
||||||
|
|
||||||
# 2) design_briefs.md
|
|
||||||
(cache_dir / "design_briefs.md").write_text(
|
|
||||||
_build_briefs_md(briefs, generated_at), encoding="utf-8")
|
|
||||||
|
|
||||||
# 3) composite_prompts.json / .md
|
|
||||||
(cache_dir / "composite_prompts.json").write_text(
|
|
||||||
json.dumps({"generated_at": generated_at, "total": len(briefs), "composite_prompts": briefs},
|
|
||||||
ensure_ascii=False, indent=2), encoding="utf-8")
|
|
||||||
(cache_dir / "composite_prompts.md").write_text(
|
|
||||||
_build_composite_md(briefs), encoding="utf-8")
|
|
||||||
|
|
||||||
# 4) report.md(本次任务报告 → 产物目录)
|
|
||||||
(output_dir / "report.md").write_text(_build_report_md(state), encoding="utf-8")
|
|
||||||
|
|
||||||
# 5) 生成纯印花设计稿(图2):前 N 个 safe 简报用 image_prompt 文生图
|
# 5) 生成纯印花设计稿(图2):前 N 个 safe 简报用 image_prompt 文生图
|
||||||
designs: List[Dict[str, Any]] = []
|
designs: List[Dict[str, Any]] = []
|
||||||
@@ -153,45 +229,20 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
design_dir = output_dir / "designs"
|
design_dir = output_dir / "designs"
|
||||||
design_dir.mkdir(exist_ok=True)
|
design_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
def _gen_one(i: int, b: Dict[str, Any]):
|
# 随机种子:config compose.seed >0 时固定(可复现,网关支持才生效);0/留空=每次随机
|
||||||
"""单张设计稿生成(并发线程内调用,每设计一线程)。
|
_seed = int(compose_cfg.get("seed") or 0)
|
||||||
|
_seed = _seed if _seed > 0 else None
|
||||||
|
|
||||||
Pinterest 参考模式:简报带 ref_images(爬取图)→ 用 ib.print() 图生图,
|
def _gen_one(i: int, b: Dict[str, Any]):
|
||||||
把爬取图 + 多模态分析简报(已封装进 image_prompt)一起发给生图模型;
|
"""单张设计稿生成(并发线程内调用,每设计一线程)。"""
|
||||||
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
|
out_path = generate_design(ib, b, design_dir, f"{country}_{i:02d}",
|
||||||
"""
|
state.get("errors"), seed=_seed,
|
||||||
try:
|
size=compose_cfg.get("design_size", "1024x1024"))
|
||||||
img_prompt = sanitize_image_prompt(b.get("image_prompt", ""))
|
if out_path is None:
|
||||||
img_prompt = ensure_rebrand_hint(b, img_prompt) # review → 原创化魔改引导
|
return i, b, None, None
|
||||||
out_path = str(design_dir / f"{country}_{i:02d}_design.png")
|
|
||||||
ref_images = [str(p) for p in (b.get("ref_images") or []) if str(p)]
|
|
||||||
if ref_images and hasattr(ib, "print"):
|
|
||||||
try:
|
|
||||||
# 图生图:以爬取图为参考,按分析简报生成原创设计(不复制原图)
|
|
||||||
ref_prompt = img_prompt + (
|
|
||||||
" Create an ORIGINAL, non-copying flat print design inspired ONLY by "
|
|
||||||
"the reference image's style and mood. Do NOT reproduce the reference "
|
|
||||||
"image, its characters, logos, or any text.")
|
|
||||||
out_path = ib.print(
|
|
||||||
ref_prompt, ref_images[0], out_path,
|
|
||||||
b.get("composite_negative", ""),
|
|
||||||
extra_images=ref_images[1:] or None,
|
|
||||||
size="1024x1024") # 印花设计统一 1024x1024
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
print(f"[compose] 图生图(参考图)失败,回退文生图 {b.get('topic','')}: {e}")
|
|
||||||
out_path = ib.generate(
|
|
||||||
img_prompt, str(design_dir / f"{country}_{i:02d}_design.png"),
|
|
||||||
b.get("composite_negative", ""), size="1024x1024")
|
|
||||||
else:
|
|
||||||
out_path = ib.generate(
|
|
||||||
img_prompt, str(design_dir / f"{country}_{i:02d}_design.png"),
|
|
||||||
b.get("composite_negative", ""), size="1024x1024")
|
|
||||||
return i, b, out_path, None
|
return i, b, out_path, None
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
return i, b, None, e
|
|
||||||
|
|
||||||
targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
|
targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
|
||||||
# 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待
|
# 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待
|
||||||
@@ -206,6 +257,8 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
state.setdefault("errors", []).append({
|
state.setdefault("errors", []).append({
|
||||||
"node": "compose", "type": type(err).__name__,
|
"node": "compose", "type": type(err).__name__,
|
||||||
"message": f"设计稿生成失败 {b.get('topic','')}: {err}", "trace": ""})
|
"message": f"设计稿生成失败 {b.get('topic','')}: {err}", "trace": ""})
|
||||||
|
elif out_path is None:
|
||||||
|
print(f"[compose] 设计稿跳过(MD5 全局去重): {b.get('topic', '')}")
|
||||||
else:
|
else:
|
||||||
b["design_path"] = out_path
|
b["design_path"] = out_path
|
||||||
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})
|
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})
|
||||||
|
|||||||
@@ -1,28 +1,71 @@
|
|||||||
"""Pinterest 参考模式节点 3/3:LLM 多模态分析图片 → 原创设计简报(pinterest_analyze)。
|
"""Pinterest 参考模式节点 3/3:从图池取图 → 多并发 LLM 分析 → 原创设计简报(pinterest_analyze)。
|
||||||
|
|
||||||
对 pinterest_scrape 爬到的每个搜索词图片,调 LLM 多模态分析(analyze_pinterest_images)
|
图池机制:
|
||||||
提取视觉概念(风格/情绪/主体/配色/构图)→ 生成原创设计简报
|
- 从持久化图池(image_pool.json)取「未消费」图片(md5 不在 used_images.json)。
|
||||||
(motif/art_style/color_palette/composition/concept/negative_prompt),
|
- 大图先压缩(内存占用过大 → 缩放/重编码)再送 LLM。
|
||||||
再经 prompt_node 装配最终 image/wearable/composite 提示词,产出标准 briefs 供 compose 用。
|
- 多并发分析(每批 analyze_per_term 张,并发 analyze_concurrency 线程)。
|
||||||
|
- 每张被分析的图片 md5 一律拉黑(used_images.json)——合适→产出简报→生成设计(设计 md5 全局拉黑见 compose);
|
||||||
|
不合适→图片 md5 已拉黑→下一轮自动取下一张,不重复分析。
|
||||||
|
- 图池无未消费图片时返回空,由路由触发新一轮搜索。
|
||||||
|
|
||||||
兜底链:LLM 多模态 → 纯文本降级(后端内部)→ mock 规则简报 → 空列表(下游跳过)。
|
兜底链:LLM 多模态 → 纯文本降级(后端内部)→ mock 规则简报 → 空列表(下游跳过)。
|
||||||
带 with_fallback:任何异常都不中断。
|
带 with_fallback:任何异常都不中断。
|
||||||
"""
|
"""
|
||||||
|
import concurrent.futures
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from graph.llms import get_backend
|
from graph.llms import get_backend
|
||||||
from graph.nodes.prompt_node import prompt_node
|
from graph.nodes.prompt_node import prompt_node
|
||||||
|
from graph.pinterest import (
|
||||||
|
compress_image,
|
||||||
|
load_image_pool,
|
||||||
|
load_used_images,
|
||||||
|
pool_unused_images,
|
||||||
|
save_used_images,
|
||||||
|
)
|
||||||
from graph.validate import with_fallback
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str) -> List[Dict[str, Any]]:
|
|
||||||
|
def _brief_suitable(b: Dict[str, Any]) -> bool:
|
||||||
|
"""简报是否适合做 T 恤印花:非侵权(blocked 拦截)+ 有主体 + 非明显非印花概念。"""
|
||||||
|
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):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str,
|
||||||
|
existing_topics: List[str] = None) -> List[Dict[str, Any]]:
|
||||||
"""富化原始简报 → screened 格式(唯一 topic / safe / 分类 / 分数),供 prompt_node 装配。
|
"""富化原始简报 → screened 格式(唯一 topic / safe / 分类 / 分数),供 prompt_node 装配。
|
||||||
|
|
||||||
同一搜索词的多张图会产出多条简报,topic 相同 → 追加序号保证唯一
|
同一搜索词的多张图会产出多条简报,topic 相同 → 追加序号保证唯一
|
||||||
(product_node 按 topic 绑定简报,重复 topic 会互相覆盖)。
|
(product_node 按 topic 绑定简报,重复 topic 会互相覆盖)。
|
||||||
|
existing_topics: 已累计简报的 topic 列表;用它初始化计数实现跨轮次去重——
|
||||||
|
直接搜固定词时每轮 LLM 都返回相同 topic,若每轮从 #1 重新计数,
|
||||||
|
30 个产品会因 topic 重复只用到前几个唯一设计(其余全复制)。
|
||||||
"""
|
"""
|
||||||
from graph.classify import classify
|
from graph.classify import classify
|
||||||
|
import re as _re
|
||||||
seen_topics: Dict[str, int] = {}
|
seen_topics: Dict[str, int] = {}
|
||||||
|
# 已累计简报按「基础词」计数(去掉 #N 后缀),保证跨轮次序号连续递增
|
||||||
|
for t in existing_topics or []:
|
||||||
|
key = _re.sub(r"\s+#\d+$", "", str(t).strip().lower())
|
||||||
|
if key:
|
||||||
|
seen_topics[key] = seen_topics.get(key, 0) + 1
|
||||||
out: List[Dict[str, Any]] = []
|
out: List[Dict[str, Any]] = []
|
||||||
for i, b in enumerate(raw_briefs):
|
for i, b in enumerate(raw_briefs):
|
||||||
if not isinstance(b, dict):
|
if not isinstance(b, dict):
|
||||||
@@ -38,9 +81,9 @@ def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str) -> List[Dict[
|
|||||||
out.append({
|
out.append({
|
||||||
"country": country,
|
"country": country,
|
||||||
"topic": topic,
|
"topic": topic,
|
||||||
"risk_level": "safe",
|
"risk_level": str(b.get("risk_level") or "safe").strip().lower() or "safe",
|
||||||
"safe_for_print": True,
|
"safe_for_print": bool(b.get("safe_for_print", True)),
|
||||||
"suitable_for_print": True,
|
"suitable_for_print": bool(b.get("suitable_for_print", True)),
|
||||||
"design_category": classify(term),
|
"design_category": classify(term),
|
||||||
"concept": str(b.get("concept") or "").strip() or f"围绕「{term}」的原创印花设计",
|
"concept": str(b.get("concept") or "").strip() or f"围绕「{term}」的原创印花设计",
|
||||||
"motif": motif,
|
"motif": motif,
|
||||||
@@ -48,7 +91,9 @@ def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str) -> List[Dict[
|
|||||||
"color_palette": str(b.get("color_palette") or "").strip(),
|
"color_palette": str(b.get("color_palette") or "").strip(),
|
||||||
"composition": str(b.get("composition") or "").strip(),
|
"composition": str(b.get("composition") or "").strip(),
|
||||||
"negative_prompt": str(b.get("negative_prompt") or "").strip(),
|
"negative_prompt": str(b.get("negative_prompt") or "").strip(),
|
||||||
|
"image_prompt": str(b.get("image_prompt") or "").strip(),
|
||||||
"ref_images": [str(p) for p in (b.get("ref_images") or []) if str(p)],
|
"ref_images": [str(p) for p in (b.get("ref_images") or []) if str(p)],
|
||||||
|
"source_md5": str(b.get("source_md5") or "").strip().lower(),
|
||||||
"slogan": "",
|
"slogan": "",
|
||||||
"score": 1.0,
|
"score": 1.0,
|
||||||
"confidence": 1.0,
|
"confidence": 1.0,
|
||||||
@@ -59,21 +104,89 @@ def _enrich_briefs(raw_briefs: List[Dict[str, Any]], country: str) -> List[Dict[
|
|||||||
|
|
||||||
@with_fallback("pinterest_analyze")
|
@with_fallback("pinterest_analyze")
|
||||||
def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
images: Dict[str, List[str]] = state.get("pinterest_images") or {}
|
|
||||||
if not images:
|
|
||||||
print("[pinterest_analyze] 无爬取图片,跳过分析")
|
|
||||||
return {"pinterest_briefs": [], "briefs": [], "errors": state.get("errors") or []}
|
|
||||||
|
|
||||||
country = state["country"]
|
country = state["country"]
|
||||||
config = state["config"]
|
config = state["config"]
|
||||||
|
output_dir = state["output_dir"]
|
||||||
errors = list(state.get("errors") or [])
|
errors = list(state.get("errors") or [])
|
||||||
|
|
||||||
pcfg = config.get("pinterest") or {}
|
pcfg = config.get("pinterest") or {}
|
||||||
analyze_per_term = int(pcfg.get("analyze_per_term", 6))
|
analyze_per_term = int(pcfg.get("analyze_per_term", 1))
|
||||||
|
concurrency = int(pcfg.get("analyze_concurrency", 3))
|
||||||
max_designs = int(pcfg.get("max_designs", 10))
|
max_designs = int(pcfg.get("max_designs", 10))
|
||||||
|
n_ref = max(1, int(pcfg.get("ref_images_per_design", 1)))
|
||||||
provider = str(pcfg.get("provider") or "openai").strip().lower()
|
provider = str(pcfg.get("provider") or "openai").strip().lower()
|
||||||
|
|
||||||
# 1) LLM 后端(openai → 真多模态;mock → 规则兜底)
|
# 按需分析:只取补齐到目标所需的图片数(batch_size 为上限,不超额分析),并按 md5 去重,
|
||||||
|
# 保证同一图片内容(md5)不会同时被多条简报使用
|
||||||
|
target = int(state.get("pinterest_target") or 0)
|
||||||
|
existing = state.get("briefs") or []
|
||||||
|
remaining = max(0, target - len(existing))
|
||||||
|
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
|
||||||
|
|
||||||
|
# 1) 图池取未消费图片(md5 不在 used_images);无 → 返回空,路由触发搜索
|
||||||
|
pool = load_image_pool(output_dir, country)
|
||||||
|
used = load_used_images(output_dir, country)
|
||||||
|
unused = pool_unused_images(pool, used)
|
||||||
|
if not unused:
|
||||||
|
print("[pinterest_analyze] 图池无未消费图片,跳过分析(路由将触发新一轮搜索)")
|
||||||
|
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:
|
||||||
|
m = str(img.get("md5") or "").strip().lower()
|
||||||
|
if m and m in seen_md5:
|
||||||
|
continue # 同一图片内容(md5)不重复分析
|
||||||
|
seen_md5.add(m)
|
||||||
|
batch.append(img)
|
||||||
|
if len(batch) >= need:
|
||||||
|
break
|
||||||
|
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。
|
||||||
|
|
||||||
|
全局 id 校验:简报必须带 image_index(对应输入第几张图,0-based);
|
||||||
|
无 image_index(mock 兜底)→ 回退按顺序;无效/越界/重复 → 丢弃该简报(避免错位)。
|
||||||
|
这样 analyze_per_term 可 >1 一次分析多张图提速,简报仍严格对应各自的图。
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
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)
|
||||||
|
refs: List[str] = []
|
||||||
|
for k in range(n_ref):
|
||||||
|
src = chunk_paths[(idx + k) % len(chunk_paths)]
|
||||||
|
if src not in refs:
|
||||||
|
refs.append(src)
|
||||||
|
b["ref_images"] = refs
|
||||||
|
b["source_md5"] = chunk_md5s[idx] if idx < len(chunk_md5s) else ""
|
||||||
|
out.append(b)
|
||||||
|
return out
|
||||||
|
|
||||||
|
# 2) LLM 后端(openai → 真多模态;mock → 规则兜底)
|
||||||
llm = None
|
llm = None
|
||||||
if provider != "static":
|
if provider != "static":
|
||||||
try:
|
try:
|
||||||
@@ -87,64 +200,123 @@ def pinterest_analyze_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
print(f"[pinterest_analyze] LLM 初始化失败: {e}")
|
print(f"[pinterest_analyze] LLM 初始化失败: {e}")
|
||||||
llm = None
|
llm = None
|
||||||
|
|
||||||
# 2) 逐搜索词分析图片 → 原始设计简报
|
# 3) 大图先压缩(内存占用过大 → 缩放/重编码),再按批分组
|
||||||
|
compressed_map: Dict[str, str] = {}
|
||||||
|
for img in batch:
|
||||||
|
compressed_map[img["path"]] = compress_image(img["path"])
|
||||||
|
chunks: List[List[Dict[str, Any]]] = [
|
||||||
|
batch[i:i + analyze_per_term] for i in range(0, len(batch), analyze_per_term)
|
||||||
|
]
|
||||||
|
|
||||||
|
# 4) 多并发分析(每线程分析一个 chunk;LLM 后端只读 self._cfg,线程安全)
|
||||||
raw_briefs: List[Dict[str, Any]] = []
|
raw_briefs: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def _analyze_chunk(chunk: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
term = str(chunk[0].get("term") or "")
|
||||||
|
paths = [compressed_map.get(img["path"], img["path"]) for img in chunk]
|
||||||
if llm is not None and hasattr(llm, "analyze_pinterest_images"):
|
if llm is not None and hasattr(llm, "analyze_pinterest_images"):
|
||||||
for term, paths in images.items():
|
|
||||||
sample = list(paths)[:analyze_per_term]
|
|
||||||
if not sample:
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
res = llm.analyze_pinterest_images(sample, term, country)
|
def _on_400():
|
||||||
res = res or []
|
pipe = state.get("pinterest_pipeline")
|
||||||
raw_briefs.extend(res)
|
if pipe is not None and hasattr(pipe, "record_400"):
|
||||||
print(f"[pinterest_analyze] 「{term}」分析 {len(sample)} 张图 → {len(res)} 条简报")
|
if pipe.record_400():
|
||||||
|
pipe._abort_current_term()
|
||||||
|
res = llm.analyze_pinterest_images(paths, term, country, on_400=_on_400) or []
|
||||||
|
# 把每条简报的来源图路径回填为原始图(压缩图仅用于分析,参考图用原图)
|
||||||
|
return _assign_refs(res, chunk)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
errors.append({"node": "pinterest_analyze", "type": type(e).__name__,
|
errors.append({"node": "pinterest_analyze", "type": type(e).__name__,
|
||||||
"message": f"term[{term}]: {e}", "trace": ""})
|
"message": f"term[{term}] batch@{len(chunk)}: {e}", "trace": ""})
|
||||||
print(f"[pinterest_analyze] 「{term}」分析失败: {e}")
|
print(f"[pinterest_analyze] 「{term}」分析失败: {e}")
|
||||||
|
return []
|
||||||
|
return []
|
||||||
|
|
||||||
# 3) 兜底:LLM 无结果 → mock 规则简报(零 API 成本,保证有设计可生成)。
|
workers = max(1, min(concurrency, len(chunks)))
|
||||||
# 注意必须切到 mock 后端,不能再调回失败的 llm(否则同样报错)。
|
if len(chunks) > 1:
|
||||||
|
print(f"[pinterest_analyze] 并发分析 {len(chunks)} 批({workers} 线程,每批 {analyze_per_term} 张)…")
|
||||||
|
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())
|
||||||
|
|
||||||
|
# 5) 兜底:LLM 无结果 → mock 规则简报(零 API 成本,保证有设计可生成)
|
||||||
if not raw_briefs:
|
if not raw_briefs:
|
||||||
try:
|
try:
|
||||||
mock = get_backend("mock")
|
mock = get_backend("mock")
|
||||||
for term, paths in images.items():
|
for chunk in chunks:
|
||||||
sample = list(paths)[:analyze_per_term]
|
term = str(chunk[0].get("term") or "")
|
||||||
if sample:
|
paths = [compressed_map.get(img["path"], img["path"]) for img in chunk]
|
||||||
raw_briefs.extend(mock.analyze_pinterest_images(sample, term, country) or [])
|
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)} 条")
|
print(f"[pinterest_analyze] 兜底:mock 规则简报 {len(raw_briefs)} 条")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[pinterest_analyze] mock 兜底失败: {e}")
|
print(f"[pinterest_analyze] mock 兜底失败: {e}")
|
||||||
|
|
||||||
# 4) 上限 + 去重(同 motif+style 指纹只留一条)
|
# 6) 本批所有图片 md5 一律拉黑(已消费,不再复用)——合适/不合适都拉黑
|
||||||
raw_briefs = raw_briefs[:max_designs]
|
for img in batch:
|
||||||
|
if img.get("md5"):
|
||||||
|
used.add(str(img["md5"]).lower())
|
||||||
|
save_used_images(output_dir, country, used)
|
||||||
|
|
||||||
|
# 7) 简报过滤:只留适合印花的(有主体 + 非明显非印花概念)
|
||||||
|
kept: List[Dict[str, Any]] = []
|
||||||
|
for b in raw_briefs:
|
||||||
|
if isinstance(b, dict) and _brief_suitable(b):
|
||||||
|
kept.append(b)
|
||||||
|
if len(kept) < len(raw_briefs):
|
||||||
|
print(f"[pinterest_analyze] 简报过滤:{len(raw_briefs)} → {len(kept)} 条适合印花")
|
||||||
|
|
||||||
|
# 8) 上限 + 去重(同 motif+style 指纹只留一条)
|
||||||
|
kept = kept[:max_designs]
|
||||||
seen: set = set()
|
seen: set = set()
|
||||||
uniq: List[Dict[str, Any]] = []
|
uniq: List[Dict[str, Any]] = []
|
||||||
for b in raw_briefs:
|
for b in kept:
|
||||||
if not isinstance(b, dict):
|
|
||||||
continue
|
|
||||||
fp = f"{str(b.get('motif', '')).strip().lower()}|{str(b.get('art_style', '')).strip().lower()}"
|
fp = f"{str(b.get('motif', '')).strip().lower()}|{str(b.get('art_style', '')).strip().lower()}"
|
||||||
if fp in seen:
|
if fp in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(fp)
|
seen.add(fp)
|
||||||
uniq.append(b)
|
uniq.append(b)
|
||||||
raw_briefs = uniq
|
kept = uniq
|
||||||
|
|
||||||
# 5) 富化 → screened → prompt_node 装配提示词 → 标准 briefs
|
|
||||||
screened = _enrich_briefs(raw_briefs, country)
|
|
||||||
if not screened:
|
|
||||||
print("[pinterest_analyze] 无有效设计简报,跳过")
|
|
||||||
return {"pinterest_briefs": [], "briefs": [], "errors": errors}
|
|
||||||
|
|
||||||
|
# 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]] = []
|
||||||
|
if screened:
|
||||||
r = prompt_node({**state, "screened": screened})
|
r = prompt_node({**state, "screened": screened})
|
||||||
briefs = r.get("briefs") or []
|
new_briefs = r.get("briefs") or []
|
||||||
|
|
||||||
|
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:]
|
||||||
|
pipe = state.get("pinterest_pipeline")
|
||||||
|
if pipe is not None and pushed:
|
||||||
|
if getattr(pipe, "is_400_aborted", lambda: False)():
|
||||||
|
print("[pinterest_analyze] 当前种子词 400 超限已放弃,本轮简报不推送")
|
||||||
|
pushed = []
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
pipe.add_briefs(pushed)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_analyze] 简报入池失败: {e}")
|
||||||
|
|
||||||
stats = dict(state.get("stats") or {})
|
stats = dict(state.get("stats") or {})
|
||||||
stats["pinterest_analyze"] = {
|
stats["pinterest_analyze"] = {
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"images_analyzed": sum(len(v) for v in images.values()),
|
"images_analyzed": len(batch),
|
||||||
"briefs": len(briefs),
|
"pool_unused": len(unused),
|
||||||
|
"used_images": len(used),
|
||||||
|
"briefs": len(new_briefs),
|
||||||
|
"accumulated": len(accumulated),
|
||||||
}
|
}
|
||||||
print(f"[pinterest_analyze] 设计简报 {len(briefs)} 条({country})")
|
print(f"[pinterest_analyze] 本轮分析 {len(batch)} 张图 → 简报 {len(new_briefs)} 条,"
|
||||||
return {"pinterest_briefs": raw_briefs, "briefs": briefs, "stats": stats, "errors": errors}
|
f"累计 {len(accumulated)} 条({country})")
|
||||||
|
return {"pinterest_briefs": kept, "briefs": accumulated, "stats": stats, "errors": errors}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""Pinterest 并发生成流水线节点 3/3:收尾(pinterest_finalize)。
|
||||||
|
|
||||||
|
分析循环结束后:排空简报池、等待后台全部产品完成(设计→三合一→OSS→种草图),
|
||||||
|
合并产品到 state,补写 compose 简报报告与 products.json,再交给 template_export。
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from graph.validate import with_fallback
|
||||||
|
|
||||||
|
|
||||||
|
@with_fallback("pinterest_finalize")
|
||||||
|
def pinterest_finalize_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
pipe = state.get("pinterest_pipeline")
|
||||||
|
products: list = []
|
||||||
|
perr: list = []
|
||||||
|
if pipe is not None:
|
||||||
|
products, perr = pipe.finish()
|
||||||
|
|
||||||
|
# 合并后台产出的产品(与已存在的合并,避免覆盖)
|
||||||
|
state_products = list(state.get("product") or [])
|
||||||
|
state_products.extend(products)
|
||||||
|
|
||||||
|
# 补写 compose 简报报告(design_briefs / composite_prompts / report.md)
|
||||||
|
try:
|
||||||
|
from graph.nodes.compose_node import write_compose_reports
|
||||||
|
write_compose_reports(state, state.get("briefs") or [])
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_finalize] 简报报告写入失败: {e}")
|
||||||
|
|
||||||
|
# 写 products.json(product 节点原职责)
|
||||||
|
try:
|
||||||
|
from graph.nodes.product_node import _write_products
|
||||||
|
prod_dir = Path(state["output_dir"]) / "product"
|
||||||
|
prod_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
_write_products(prod_dir, state_products)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_finalize] products.json 写入失败: {e}")
|
||||||
|
|
||||||
|
stats = dict(state.get("stats") or {})
|
||||||
|
stats["pinterest_pipeline"] = {"products": len(products), "errors": len(perr)}
|
||||||
|
errors = list(state.get("errors") or []) + perr
|
||||||
|
oss_seq = getattr(pipe, "oss_seq", state.get("oss_seq", 0)) if pipe is not None \
|
||||||
|
else state.get("oss_seq", 0)
|
||||||
|
print(f"[pinterest_finalize] 收尾完成:合并 {len(state_products)} 个产品,"
|
||||||
|
f"后台错误 {len(perr)},oss_seq={oss_seq}")
|
||||||
|
return {"product": state_products, "oss_seq": oss_seq, "errors": errors, "stats": stats}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Pinterest 并发生成流水线节点 0/3:初始化简报池(pinterest_init)。
|
||||||
|
|
||||||
|
创建 PinterestPipeline(简报池 + 后台并发生成线程),存 state["pinterest_pipeline"],
|
||||||
|
供 pinterest_analyze 推送简报、pinterest_finalize 收尾。
|
||||||
|
"""
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from graph.validate import with_fallback
|
||||||
|
|
||||||
|
|
||||||
|
@with_fallback("pinterest_init")
|
||||||
|
def pinterest_init_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
from graph.pinterest_pipeline import PinterestPipeline
|
||||||
|
pipe = PinterestPipeline(state)
|
||||||
|
return {"pinterest_pipeline": pipe}
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
"""Pinterest 参考模式节点 2/3:爬取图片(pinterest_scrape)。
|
"""Pinterest 参考模式节点 2/3:爬取图片(pinterest_scrape)。
|
||||||
|
|
||||||
对 pinterest_search 生成的每个搜索词,调 pinterest_scraper.scraper.scrape_pinterest
|
对 pinterest_search 生成的搜索词(按需:每次 1 个),调 pinterest_scraper.scraper.scrape_pinterest
|
||||||
(Playwright 启动本地 Chrome)搜索 Pinterest 并下载图片到
|
(Playwright 启动本地 Chrome)搜索 Pinterest 并下载图片到 output/pinterest_ref/<国家>/<搜索词>/。
|
||||||
output/pinterest_ref/<国家>/<搜索词>/。
|
|
||||||
|
|
||||||
- 单个搜索词失败(未登录/网络/无结果)跳过,不中断整批。
|
- 单个搜索词失败(未登录/网络/无结果)跳过,不中断整批。
|
||||||
- 并发数由 config.pinterest.scrape_concurrency 控制(每个并发开一个 Chrome 窗口)。
|
- 只有用了才标记已用:爬取成功(真正用掉该搜索词)→ 持久化已用词;
|
||||||
|
爬取失败 → 记入本轮 attempted(不持久化),避免同轮重复生成。
|
||||||
- 已爬取过且图片数达标的搜索词跳过(断点续爬,避免重复开 Chrome)。
|
- 已爬取过且图片数达标的搜索词跳过(断点续爬,避免重复开 Chrome)。
|
||||||
"""
|
"""
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from graph.pinterest import (
|
||||||
|
image_md5,
|
||||||
|
load_image_pool,
|
||||||
|
load_used_images,
|
||||||
|
load_used_terms,
|
||||||
|
merge_used,
|
||||||
|
save_image_pool,
|
||||||
|
save_used_terms,
|
||||||
|
)
|
||||||
from graph.validate import with_fallback
|
from graph.validate import with_fallback
|
||||||
|
|
||||||
|
|
||||||
@@ -45,6 +54,7 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
concurrency = int(pcfg.get("scrape_concurrency", 2))
|
concurrency = int(pcfg.get("scrape_concurrency", 2))
|
||||||
headless = bool(pcfg.get("headless", False))
|
headless = bool(pcfg.get("headless", False))
|
||||||
proxy = pcfg.get("proxy") or None
|
proxy = pcfg.get("proxy") or None
|
||||||
|
search_mode = str(pcfg.get("search_mode") or "direct").strip().lower()
|
||||||
|
|
||||||
# 所有搜索词共享同一个 .chrome_session 登录态目录,Chrome 对同一 user-data-dir 是单例,
|
# 所有搜索词共享同一个 .chrome_session 登录态目录,Chrome 对同一 user-data-dir 是单例,
|
||||||
# 并发启动会互相抢占导致 "browser has been closed",必须串行爬取。
|
# 并发启动会互相抢占导致 "browser has been closed",必须串行爬取。
|
||||||
@@ -59,16 +69,22 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
proxy = detect_proxy() or get_system_proxy()
|
proxy = detect_proxy() or get_system_proxy()
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
proxy = None
|
proxy = None
|
||||||
if proxy and not _validate_proxy(proxy):
|
if not proxy:
|
||||||
|
print("[pinterest_scrape] 警告:未检测到代理,将直连下载。国内网络通常无法访问 "
|
||||||
|
"i.pinimg.com,请先开启代理/VPN(Clash/v2ray 等)再运行,否则图片下载会全部失败")
|
||||||
|
elif not _validate_proxy(proxy):
|
||||||
print(f"[pinterest_scrape] 警告:代理 {proxy} 无法连通外网,请检查代理/VPN 是否正常,"
|
print(f"[pinterest_scrape] 警告:代理 {proxy} 无法连通外网,请检查代理/VPN 是否正常,"
|
||||||
f"否则 Pinterest 将无法访问(爬取会失败)")
|
f"否则 Pinterest 将无法访问(爬取会失败)")
|
||||||
|
|
||||||
results: Dict[str, List[str]] = {}
|
results: Dict[str, List[str]] = {}
|
||||||
skipped: List[str] = []
|
skipped: List[str] = []
|
||||||
|
failed: List[str] = []
|
||||||
|
|
||||||
def _one(term: str) -> None:
|
def _one(term: str) -> None:
|
||||||
term_dir = _term_dir(output_dir, country, term)
|
term_dir = _term_dir(output_dir, country, term)
|
||||||
if _already_scraped(term_dir):
|
# direct 模式:固定关键词允许重复爬取(图池不足时自动再搜,Pinterest 每次可能返回不同图);
|
||||||
|
# llm 模式:已爬取过且达标 → 跳过(断点续爬,避免重复开 Chrome)
|
||||||
|
if search_mode != "direct" and _already_scraped(term_dir):
|
||||||
skipped.append(term)
|
skipped.append(term)
|
||||||
print(f"[pinterest_scrape] 已爬取过(跳过): {term}")
|
print(f"[pinterest_scrape] 已爬取过(跳过): {term}")
|
||||||
return
|
return
|
||||||
@@ -78,6 +94,7 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
save_dir=str(term_dir), proxy=proxy, headless=headless)
|
save_dir=str(term_dir), proxy=proxy, headless=headless)
|
||||||
results[term] = files
|
results[term] = files
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
failed.append(term)
|
||||||
errors.append({"node": "pinterest_scrape", "type": type(e).__name__,
|
errors.append({"node": "pinterest_scrape", "type": type(e).__name__,
|
||||||
"message": f"term[{term}]: {e}", "trace": ""})
|
"message": f"term[{term}]: {e}", "trace": ""})
|
||||||
print(f"[pinterest_scrape] 爬取失败(跳过): {term}: {e}")
|
print(f"[pinterest_scrape] 爬取失败(跳过): {term}: {e}")
|
||||||
@@ -86,12 +103,55 @@ def pinterest_scrape_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max(1, concurrency)) as ex:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max(1, concurrency)) as ex:
|
||||||
list(ex.map(_one, terms))
|
list(ex.map(_one, terms))
|
||||||
|
|
||||||
|
# 只有用了才标记已用:爬取成功(含已爬取跳过)的词 → 持久化已用;失败词 → 本轮 attempted(不持久化)
|
||||||
|
# direct 模式:固定关键词不拉黑(可跨轮复用),仅 llm 模式持久化已用词
|
||||||
|
used = load_used_terms(output_dir, country)
|
||||||
|
consumed = (list(results.keys()) + skipped) if search_mode != "direct" else []
|
||||||
|
new_used = merge_used(used, consumed)
|
||||||
|
if new_used != used:
|
||||||
|
save_used_terms(output_dir, country, new_used)
|
||||||
|
print(f"[pinterest_scrape] 已用搜索词更新:新增 {len(consumed)} 个,累计 {len(new_used)}")
|
||||||
|
attempted = merge_used(state.get("pinterest_attempted") or [], failed)
|
||||||
|
|
||||||
|
# 新爬取的图片注册进图池(含 md5),供分析节点按需取用;
|
||||||
|
# 进图池前做 md5 校验去重:md5 已存在于图池 / 已拉黑(used_images)/ 本批重复 → 跳过
|
||||||
|
pool = load_image_pool(output_dir, country)
|
||||||
|
existing = pool.get("images") or []
|
||||||
|
known_paths = {str(img.get("path")) for img in existing}
|
||||||
|
known_md5s = {str(img.get("md5") or "").strip().lower() for img in existing}
|
||||||
|
used_md5s = load_used_images(output_dir, country)
|
||||||
|
new_imgs: List[Dict[str, Any]] = []
|
||||||
|
seen_md5: set = set()
|
||||||
|
for term, files in results.items():
|
||||||
|
for f in files:
|
||||||
|
if f in known_paths:
|
||||||
|
continue
|
||||||
|
m = str(image_md5(f) or "").strip().lower()
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
if m in known_md5s or m in used_md5s or m in seen_md5:
|
||||||
|
print(f"[pinterest_scrape] 图池 md5 去重跳过: {f}")
|
||||||
|
continue
|
||||||
|
seen_md5.add(m)
|
||||||
|
new_imgs.append({"path": f, "md5": m, "term": term})
|
||||||
|
if new_imgs:
|
||||||
|
pool["images"] = existing + new_imgs
|
||||||
|
save_image_pool(output_dir, country, pool)
|
||||||
|
print(f"[pinterest_scrape] 图池新增 {len(new_imgs)} 张图片,累计 {len(pool['images'])} 张")
|
||||||
|
|
||||||
|
# 新一批图爬取完成 → 重置 400 计数(per 种子词),记录当前种子词
|
||||||
|
pipe = state.get("pinterest_pipeline")
|
||||||
|
if pipe is not None and hasattr(pipe, "reset_400"):
|
||||||
|
for term in results.keys():
|
||||||
|
pipe.reset_400(term)
|
||||||
|
|
||||||
total = sum(len(v) for v in results.values())
|
total = sum(len(v) for v in results.values())
|
||||||
stats = dict(state.get("stats") or {})
|
stats = dict(state.get("stats") or {})
|
||||||
stats["pinterest_scrape"] = {
|
stats["pinterest_scrape"] = {
|
||||||
"terms": len(terms), "scraped": len(results), "skipped": len(skipped),
|
"terms": len(terms), "scraped": len(results), "skipped": len(skipped),
|
||||||
"images": total,
|
"failed": len(failed), "images": total, "pool": len(pool.get("images") or []),
|
||||||
}
|
}
|
||||||
print(f"[pinterest_scrape] 完成:{len(results)} 个搜索词,共 {total} 张图(跳过 {len(skipped)})")
|
print(f"[pinterest_scrape] 完成:{len(results)} 个搜索词,共 {total} 张图(跳过 {len(skipped)},失败 {len(failed)})")
|
||||||
|
|
||||||
return {"pinterest_images": results, "stats": stats, "errors": errors}
|
return {"pinterest_images": results, "pinterest_attempted": attempted,
|
||||||
|
"stats": stats, "errors": errors}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Pinterest 参考模式节点 1/3:LLM 生成搜索词(pinterest_search)。
|
"""Pinterest 参考模式节点 1/3:按需生成单个搜索词(pinterest_search)。
|
||||||
|
|
||||||
流程:国家 Pinterest 种子词池 → LLM 生成搜索词(json_schema 结构化 + 动态注入已用词防重复)
|
按需搜索:每次只生成 1 个搜索词(LLM json_schema + 动态注入已用词防重复),
|
||||||
→ 全局过滤(已用/黑名单/不适合T恤/去重)→ 持久化已用词。
|
带短袖/印花设计引导,保证搜索词适合短袖 T 恤印花。
|
||||||
|
不在此处持久化已用词 —— 只有爬取成功(真正用掉)才标记已用(见 pinterest_scrape)。
|
||||||
|
|
||||||
兜底链:LLM json_schema → json_object → 解析失败/调用失败 → 回退种子词池随机抽样。
|
兜底链:LLM json_schema → json_object → 解析失败/调用失败 → 回退种子词池随机抽样。
|
||||||
带 with_fallback:任何异常都不中断,返回空列表由下游跳过。
|
带 with_fallback:任何异常都不中断,返回空列表由下游跳过。
|
||||||
@@ -15,7 +16,6 @@ from graph.pinterest import (
|
|||||||
load_used_terms,
|
load_used_terms,
|
||||||
merge_used,
|
merge_used,
|
||||||
sample_seeds,
|
sample_seeds,
|
||||||
save_used_terms,
|
|
||||||
)
|
)
|
||||||
from graph.validate import with_fallback
|
from graph.validate import with_fallback
|
||||||
|
|
||||||
@@ -32,22 +32,38 @@ def pinterest_search_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
return {"pinterest_search_terms": [], "errors": errors}
|
return {"pinterest_search_terms": [], "errors": errors}
|
||||||
|
|
||||||
provider = str(pcfg.get("provider") or "openai").strip().lower()
|
provider = str(pcfg.get("provider") or "openai").strip().lower()
|
||||||
want = int(pcfg.get("search_terms_per_run", 10))
|
search_mode = str(pcfg.get("search_mode") or "direct").strip().lower()
|
||||||
|
want = int(pcfg.get("search_terms_per_run", 1)) # 每次搜索词数量
|
||||||
seed_sample = int(pcfg.get("seed_sample", 40))
|
seed_sample = int(pcfg.get("seed_sample", 40))
|
||||||
max_used_in_prompt = int(pcfg.get("max_used_terms_in_prompt", 100))
|
max_used_in_prompt = int(pcfg.get("max_used_terms_in_prompt", 100))
|
||||||
blacklist = config.get("blacklist") or []
|
blacklist = config.get("blacklist") or []
|
||||||
|
|
||||||
# 1) 种子词池(随机抽样)+ 已用搜索词
|
# 1) 种子词池 + 已用搜索词 + 本轮已尝试词(防同轮重复,不持久化)
|
||||||
seeds = sample_seeds(country, seed_sample)
|
seeds = sample_seeds(country, seed_sample)
|
||||||
used = load_used_terms(output_dir, country)
|
used = load_used_terms(output_dir, country)
|
||||||
|
attempted = [str(t).strip() for t in (state.get("pinterest_attempted") or []) if str(t).strip()]
|
||||||
|
rounds = int(state.get("pinterest_rounds") or 0) + 1
|
||||||
if not seeds:
|
if not seeds:
|
||||||
print(f"[pinterest_search] {country} 无种子词,跳过搜索词生成")
|
print(f"[pinterest_search] {country} 无种子词,跳过搜索词生成")
|
||||||
return {"pinterest_search_terms": [], "errors": errors}
|
return {"pinterest_search_terms": [], "pinterest_rounds": rounds, "errors": errors}
|
||||||
|
|
||||||
# 2) LLM 生成(json_schema + 动态注入已用词)
|
# 2) 生成搜索词:种子词不再由 LLM 给出,直接由内置国家种子词库随机抽取(优先未用过),
|
||||||
# 已用词只取最近 N 个(默认 100)注入提示词,防 token 超限;过滤仍用全量。
|
# 追加 " t-shirt design"(保证 Pinterest 返回真正的 T 恤印花图);llm 模式保留兼容
|
||||||
used_llm = used[-max_used_in_prompt:] if max_used_in_prompt > 0 else []
|
|
||||||
terms: List[str] = []
|
terms: List[str] = []
|
||||||
|
if search_mode == "direct":
|
||||||
|
from graph.pinterest import load_pinterest_seeds
|
||||||
|
pool = load_pinterest_seeds(country)
|
||||||
|
used_set = {str(u).strip().lower() for u in merge_used(used, attempted)}
|
||||||
|
fresh = [s for s in pool if s.lower() not in used_set]
|
||||||
|
if not fresh:
|
||||||
|
fresh = pool # 库内词全部用过 → 允许复用(词库有限)
|
||||||
|
terms = [f"{s} t-shirt design" if "t-shirt design" not in s.lower() else s
|
||||||
|
for s in random.sample(fresh, min(want, len(fresh)))]
|
||||||
|
print(f"[pinterest_search] direct 模式:国家种子词库随机抽 {len(terms)} 个 + t-shirt design({country})")
|
||||||
|
else:
|
||||||
|
used_llm = merge_used(used, attempted)
|
||||||
|
if max_used_in_prompt > 0:
|
||||||
|
used_llm = used_llm[-max_used_in_prompt:]
|
||||||
llm = None
|
llm = None
|
||||||
if provider != "static":
|
if provider != "static":
|
||||||
try:
|
try:
|
||||||
@@ -66,38 +82,38 @@ def pinterest_search_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
ctx = {"country": country, "seeds": seeds, "used_terms": used_llm, "count": want}
|
ctx = {"country": country, "seeds": seeds, "used_terms": used_llm, "count": want}
|
||||||
res = llm.generate_pinterest_terms(ctx)
|
res = llm.generate_pinterest_terms(ctx)
|
||||||
terms = [str(t).strip() for t in (res.get("search_terms") or []) if str(t).strip()]
|
terms = [str(t).strip() for t in (res.get("search_terms") or []) if str(t).strip()]
|
||||||
print(f"[pinterest_search] LLM 生成搜索词 {len(terms)} 个({country},已用词注入 {len(used_llm)}/{len(used)})")
|
print(f"[pinterest_search] LLM 生成搜索词 {len(terms)} 个({country},已用词注入 {len(used_llm)})")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[pinterest_search] LLM 生成失败,回退种子词池: {e}")
|
print(f"[pinterest_search] LLM 生成失败,回退种子词池: {e}")
|
||||||
terms = []
|
terms = []
|
||||||
|
|
||||||
# 3) 兜底:LLM 无结果 → 种子词池随机抽样
|
# 3) 兜底:LLM 无结果 → 种子词池随机抽样
|
||||||
if not terms:
|
if not terms:
|
||||||
terms = random.sample(seeds, min(want, len(seeds))) if seeds else []
|
terms = [f"{s} t-shirt design" if "t-shirt design" not in s.lower() else s
|
||||||
|
for s in random.sample(seeds, min(want, len(seeds)))]
|
||||||
print(f"[pinterest_search] 兜底:从种子词池取 {len(terms)} 个")
|
print(f"[pinterest_search] 兜底:从种子词池取 {len(terms)} 个")
|
||||||
|
|
||||||
# 4) 全局过滤(已用/黑名单/不适合T恤/去重)
|
# 4) 全局过滤(已用/本轮已尝试/黑名单/不适合T恤/去重)——注意:不在此处持久化已用词
|
||||||
filtered = filter_search_terms(terms, used, blacklist)
|
# direct 模式:抽样时已避开已用词(库内词有限,全部用过后允许复用),不再额外过滤
|
||||||
if len(filtered) < want and seeds:
|
if search_mode == "direct":
|
||||||
# 不足时用种子词池补充(同样过滤),保证数量
|
filtered = terms
|
||||||
extra = filter_search_terms(seeds, merge_used(used, filtered), blacklist)
|
else:
|
||||||
for t in extra:
|
filtered = filter_search_terms(terms, merge_used(used, attempted), blacklist)
|
||||||
if len(filtered) >= want:
|
if not filtered and seeds:
|
||||||
break
|
# 生成词全被过滤 → 从种子词池补充(同样过滤)
|
||||||
filtered.append(t)
|
extra = filter_search_terms(seeds, merge_used(used, attempted), blacklist)
|
||||||
|
filtered = extra[:want]
|
||||||
# 5) 持久化已用词
|
|
||||||
new_used = merge_used(used, filtered)
|
|
||||||
save_used_terms(output_dir, country, new_used)
|
|
||||||
|
|
||||||
stats = dict(state.get("stats") or {})
|
stats = dict(state.get("stats") or {})
|
||||||
stats["pinterest_search"] = {
|
stats["pinterest_search"] = {
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
|
"round": rounds,
|
||||||
"generated": len(terms),
|
"generated": len(terms),
|
||||||
"filtered": len(filtered),
|
"filtered": len(filtered),
|
||||||
"used_total": len(new_used),
|
"used_total": len(used),
|
||||||
}
|
}
|
||||||
print(f"[pinterest_search] 搜索词 {len(filtered)} 个(已用累计 {len(new_used)}): "
|
print(f"[pinterest_search] 第 {rounds} 轮搜索词 {len(filtered)} 个(已用累计 {len(used)}): "
|
||||||
f"{', '.join(filtered[:6])}{'...' if len(filtered) > 6 else ''}")
|
f"{', '.join(filtered[:3])}{'...' if len(filtered) > 3 else ''}")
|
||||||
|
|
||||||
return {"pinterest_search_terms": filtered, "stats": stats, "errors": errors}
|
return {"pinterest_search_terms": filtered, "pinterest_rounds": rounds,
|
||||||
|
"stats": stats, "errors": errors}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ def _retry_image(fn, *args, attempts: int = 3, backoff=(5, 20, 40), **kwargs):
|
|||||||
def _process_spu(
|
def _process_spu(
|
||||||
db_path, basemap_root, material_root, category, prod_dir, brief, ib,
|
db_path, basemap_root, material_root, category, prod_dir, brief, ib,
|
||||||
spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="",
|
spu, sku_code, pcfg, errors, shared_design=None, title_backend=None, country="",
|
||||||
img_code="", model_img=None,
|
img_code="", model_img=None, design_size="1024x1024", compose_size="1536x2048",
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
|
"""处理单个款号:选色 → 底图 → 设计稿 → (mark==1) 模特 → 合成 → 模板导出。
|
||||||
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
|
shared_design: compose 节点生成的纯印花设计稿路径(图2);为 None 时回退本节点 generate。
|
||||||
@@ -210,7 +210,12 @@ def _process_spu(
|
|||||||
prompt = ensure_rebrand_hint(brief, sanitize_image_prompt(brief.get("image_prompt", "")))
|
prompt = ensure_rebrand_hint(brief, sanitize_image_prompt(brief.get("image_prompt", "")))
|
||||||
ib.generate(prompt, design_path,
|
ib.generate(prompt, design_path,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
size="1024x1024") # 印花设计统一 1024x1024
|
size=design_size) # 设计稿尺寸按 config compose.design_size
|
||||||
|
# 全局 MD5 去重:生成了设计后,把 MD5 加入全局过滤(对所有国家生效);重复 → 跳过该产品
|
||||||
|
from graph.pinterest import design_md5_ok
|
||||||
|
if not design_md5_ok(design_path):
|
||||||
|
print(f"{tag} 设计稿 MD5 全局重复,跳过该产品: {design_path}")
|
||||||
|
return None
|
||||||
result["design_path"] = design_path
|
result["design_path"] = design_path
|
||||||
result["design_from"] = "product"
|
result["design_from"] = "product"
|
||||||
print(f"{tag} 纯印花设计稿已生成(product 节点): {design_path}")
|
print(f"{tag} 纯印花设计稿已生成(product 节点): {design_path}")
|
||||||
@@ -252,7 +257,7 @@ def _process_spu(
|
|||||||
ib.print(wear_prompt, str(model_img), composite_path,
|
ib.print(wear_prompt, str(model_img), composite_path,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
extra_images=[design_path, str(basemap_img)], # 图2印花, 图3底图
|
extra_images=[design_path, str(basemap_img)], # 图2印花, 图3底图
|
||||||
size="1504x2000") # 三合一统一 1504x2000
|
size=compose_size) # 合成图尺寸按 config compose.size
|
||||||
result["composite_path"] = composite_path
|
result["composite_path"] = composite_path
|
||||||
print(f"{tag} 三图模特合成图已生成(耗时 {int(time.time()-t0)}s): {composite_path}")
|
print(f"{tag} 三图模特合成图已生成(耗时 {int(time.time()-t0)}s): {composite_path}")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
@@ -260,7 +265,7 @@ def _process_spu(
|
|||||||
print(f"{tag} 三图合成失败,退避重试…: {e}")
|
print(f"{tag} 三图合成失败,退避重试…: {e}")
|
||||||
retried = _retry_image(ib.print, wear_prompt, str(model_img), composite_path,
|
retried = _retry_image(ib.print, wear_prompt, str(model_img), composite_path,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
extra_images=[design_path, str(basemap_img)], size="1504x2000")
|
extra_images=[design_path, str(basemap_img)], size=compose_size)
|
||||||
if retried is not None:
|
if retried is not None:
|
||||||
result["composite_path"] = composite_path
|
result["composite_path"] = composite_path
|
||||||
print(f"{tag} 三图合成重试成功(耗时 {int(time.time()-t0)}s): {composite_path}")
|
print(f"{tag} 三图合成重试成功(耗时 {int(time.time()-t0)}s): {composite_path}")
|
||||||
@@ -280,14 +285,14 @@ def _process_spu(
|
|||||||
ib.print(flat_prompt, str(basemap_img), printed_path,
|
ib.print(flat_prompt, str(basemap_img), printed_path,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
extra_images=[design_path], # 图2印花
|
extra_images=[design_path], # 图2印花
|
||||||
size="1504x2000") # 合成统一 1504x2000
|
size=compose_size) # 合成图尺寸按 config compose.size
|
||||||
result["printed_path"] = printed_path
|
result["printed_path"] = printed_path
|
||||||
print(f"{tag} 平铺服装图已生成(无模特,底图+印花): {printed_path}")
|
print(f"{tag} 平铺服装图已生成(无模特,底图+印花): {printed_path}")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"{tag} 平铺服装图失败,退避重试…: {e}")
|
print(f"{tag} 平铺服装图失败,退避重试…: {e}")
|
||||||
retried = _retry_image(ib.print, flat_prompt, str(basemap_img), printed_path,
|
retried = _retry_image(ib.print, flat_prompt, str(basemap_img), printed_path,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
extra_images=[design_path], size="1504x2000")
|
extra_images=[design_path], size=compose_size)
|
||||||
if retried is not None:
|
if retried is not None:
|
||||||
result["printed_path"] = printed_path
|
result["printed_path"] = printed_path
|
||||||
print(f"{tag} 平铺服装图重试成功: {printed_path}")
|
print(f"{tag} 平铺服装图重试成功: {printed_path}")
|
||||||
@@ -312,7 +317,7 @@ def _process_spu(
|
|||||||
ib.print(MODEL_WEAR_PROMPT, str(model_img), cp,
|
ib.print(MODEL_WEAR_PROMPT, str(model_img), cp,
|
||||||
brief.get("composite_negative", ""),
|
brief.get("composite_negative", ""),
|
||||||
extra_images=[design_path, str(bm)], # 图2印花, 图3该色底图
|
extra_images=[design_path, str(bm)], # 图2印花, 图3该色底图
|
||||||
size="1504x2000") # 三合一统一 1504x2000
|
size=compose_size) # 合成图尺寸按 config compose.size
|
||||||
col = next((c["color"] for c in colors if c["sku_code"] == sc), sc)
|
col = next((c["color"] for c in colors if c["sku_code"] == sc), sc)
|
||||||
color_composites.append({"sku_code": sc, "color": col, "composite_path": cp})
|
color_composites.append({"sku_code": sc, "color": col, "composite_path": cp})
|
||||||
print(f"{tag} 颜色 {sc}({col})三合一已生成: {cp}")
|
print(f"{tag} 颜色 {sc}({col})三合一已生成: {cp}")
|
||||||
@@ -327,12 +332,14 @@ def _process_spu(
|
|||||||
or result.get("design_path"))
|
or result.get("design_path"))
|
||||||
if title_img:
|
if title_img:
|
||||||
t = title_backend.generate_title(title_img, country=country)
|
t = title_backend.generate_title(title_img, country=country)
|
||||||
if t.get("en_title") or t.get("cn_title") or t.get("ja_title"):
|
if t.get("en_title") or t.get("cn_title") or t.get("ja_title") or t.get("es_title"):
|
||||||
result["en_title"] = t.get("en_title", "")
|
result["en_title"] = t.get("en_title", "")
|
||||||
result["cn_title"] = t.get("cn_title", "")
|
result["cn_title"] = t.get("cn_title", "")
|
||||||
result["ja_title"] = t.get("ja_title", "")
|
result["ja_title"] = t.get("ja_title", "")
|
||||||
|
result["es_title"] = t.get("es_title", "")
|
||||||
print(f"{tag} 标题已生成: EN={t.get('en_title','')[:50]}... "
|
print(f"{tag} 标题已生成: EN={t.get('en_title','')[:50]}... "
|
||||||
f"CN={t.get('cn_title','')[:30]}... JA={t.get('ja_title','')[:30]}...")
|
f"CN={t.get('cn_title','')[:30]}... JA={t.get('ja_title','')[:30]}... "
|
||||||
|
f"ES={t.get('es_title','')[:30]}...")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -514,7 +521,9 @@ def product_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
r = _process_spu(db_path, basemap_root, material_root, category, prod_dir,
|
r = _process_spu(db_path, basemap_root, material_root, category, prod_dir,
|
||||||
tb, ib, spu, skus, pcfg, errors, design_path, title_backend,
|
tb, ib, spu, skus, pcfg, errors, design_path, title_backend,
|
||||||
country, img_code=img_code,
|
country, img_code=img_code,
|
||||||
model_img=model_assign.get(spu.get("code", "")))
|
model_img=model_assign.get(spu.get("code", "")),
|
||||||
|
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:
|
if r:
|
||||||
r["img_code"] = img_code
|
r["img_code"] = img_code
|
||||||
return r, img_code
|
return r, img_code
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ from graph.style_rules import derive_style_palette, derive_composition
|
|||||||
from graph.templates import assemble_prompts
|
from graph.templates import assemble_prompts
|
||||||
from graph.validate import validate_brief, with_fallback
|
from graph.validate import validate_brief, with_fallback
|
||||||
|
|
||||||
|
# —— Pinterest 图生图生最终设计稿时统一追加的「小印花 + 纯白底」约束段 ——
|
||||||
|
# (从热点搜集的文字生图模板里提炼:尺寸缩小、禁止自带背景/满幅)
|
||||||
|
PINTEREST_PRINT_SUFFIX = (
|
||||||
|
" standalone pure print design on a pure white background, "
|
||||||
|
"the print artwork is SMALL and CENTERED with clearly larger white margins around it, "
|
||||||
|
"print area between about 15x18 cm and 26x32 cm, "
|
||||||
|
"do NOT fill the entire canvas, do NOT force full-bleed, "
|
||||||
|
"do NOT add any gradient, texture or background color behind the artwork, "
|
||||||
|
"no garment, no shirt, no model, no mannequin, no watermark"
|
||||||
|
)
|
||||||
|
|
||||||
# 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif,
|
# 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif,
|
||||||
# 避免 gpt-image 等内容策略频繁拦截导致"生图限制多")
|
# 避免 gpt-image 等内容策略频繁拦截导致"生图限制多")
|
||||||
_IMG_RISKY_SWAP = {
|
_IMG_RISKY_SWAP = {
|
||||||
@@ -62,6 +73,12 @@ def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
composition = (r.get("composition") or derive_composition(r["topic"], r.get("design_category"))).strip()
|
composition = (r.get("composition") or derive_composition(r["topic"], r.get("design_category"))).strip()
|
||||||
|
|
||||||
prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country)
|
prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country)
|
||||||
|
# Pinterest 简报:直接用 LLM 多模态对图片的描述拼接的 image_prompt(跳过四要素模板),
|
||||||
|
# 仅追加统一的「小印花 + 纯白底」约束段;wearable/composite 仍用模板装配。
|
||||||
|
llm_ip = (r.get("image_prompt") or "").strip()
|
||||||
|
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 时,随机注入文字段到设计稿提示词
|
# 文字印花(约 30% 概率):简报有 slogan 时,随机注入文字段到设计稿提示词
|
||||||
slogan = (r.get("slogan") or "").strip()
|
slogan = (r.get("slogan") or "").strip()
|
||||||
if slogan and random.random() < float(config.get("prompt_templates", {}).get("text_ratio", 0.3)):
|
if slogan and random.random() < float(config.get("prompt_templates", {}).get("text_ratio", 0.3)):
|
||||||
|
|||||||
@@ -40,16 +40,6 @@ def _plan_seed_shots(comps: List[Dict[str, Any]], count: int) -> List[tuple]:
|
|||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
|
||||||
def _color_tag(cc: Dict[str, Any], idx: int) -> str:
|
|
||||||
"""种草图文件名里的颜色标识:优先 sku_code 的颜色段,回退颜色名/序号。"""
|
|
||||||
sku = str(cc.get("sku_code") or "")
|
|
||||||
if "-" in sku:
|
|
||||||
tag = sku.split("-", 1)[1]
|
|
||||||
else:
|
|
||||||
tag = str(cc.get("color") or "") or f"c{idx}"
|
|
||||||
return "".join(ch for ch in tag if ch.isalnum() or ch in "-_") or f"c{idx}"
|
|
||||||
|
|
||||||
|
|
||||||
@with_fallback("seed_shot")
|
@with_fallback("seed_shot")
|
||||||
def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
products: List[Dict[str, Any]] = state.get("product") or []
|
products: List[Dict[str, Any]] = state.get("product") or []
|
||||||
@@ -95,7 +85,7 @@ def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print(f"[seed_shot] 材质读取失败(用空): {e}")
|
print(f"[seed_shot] 材质读取失败(用空): {e}")
|
||||||
|
|
||||||
from graph.seed_shot import generate_seed_shots
|
from graph.seed_shot import generate_seed_shots, read_template_category, gender_from_category
|
||||||
from graph.oss_upload import build_oss_key, compress_for_oss, upload_to_oss
|
from graph.oss_upload import build_oss_key, compress_for_oss, upload_to_oss
|
||||||
from graph.nodes.oss_upload_node import _gen_rand4, MAX_CODE
|
from graph.nodes.oss_upload_node import _gen_rand4, MAX_CODE
|
||||||
|
|
||||||
@@ -104,7 +94,18 @@ def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
seq = int(state.get("oss_seq") or 0)
|
seq = int(state.get("oss_seq") or 0)
|
||||||
oss_cfg = config.get("oss") or {}
|
oss_cfg = config.get("oss") or {}
|
||||||
oss_enabled = bool(oss_cfg.get("enabled", True)) and bool(oss_cfg.get("oss_bucket"))
|
oss_enabled = bool(oss_cfg.get("enabled", True)) and bool(oss_cfg.get("oss_bucket"))
|
||||||
size = str(ss_cfg.get("size") or "1504x2000")
|
size = str(ss_cfg.get("size") or "1536x2048")
|
||||||
|
|
||||||
|
# 类目 → 性别:模版「类目」表头值含「男」→ 男模;含「女」→ 女模;都不含 → 全部随机
|
||||||
|
gender = None
|
||||||
|
tp = str(((config.get("product") or {}).get("template_path")) or "").strip()
|
||||||
|
if tp:
|
||||||
|
category = read_template_category(tp)
|
||||||
|
gender = gender_from_category(category)
|
||||||
|
if gender:
|
||||||
|
print(f"[seed_shot] 类目「{category[:30]}…」含{'男' if gender == 'male' else '女'} → 固定 {gender} 模特")
|
||||||
|
elif category:
|
||||||
|
print(f"[seed_shot] 类目「{category[:30]}…」无男/女 → 男女模特随机")
|
||||||
|
|
||||||
all_shots: List[Dict[str, Any]] = []
|
all_shots: List[Dict[str, Any]] = []
|
||||||
shot_dir = output_dir / "seed_shots"
|
shot_dir = output_dir / "seed_shots"
|
||||||
@@ -128,7 +129,9 @@ def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
plan = _plan_seed_shots(comps, count)
|
plan = _plan_seed_shots(comps, count)
|
||||||
cn = (r.get("cn_title") or "").strip() or r.get("topic", "")
|
cn = (r.get("cn_title") or "").strip() or r.get("topic", "")
|
||||||
material = material_map.get(r.get("spu_code", ""), "")
|
material = material_map.get(r.get("spu_code", ""), "")
|
||||||
|
# 按对应货号命名(img_code=货号,如 DG000);无货号时回退 seed
|
||||||
base_prefix = r.get("img_code") or r.get("oss_code") or ""
|
base_prefix = r.get("img_code") or r.get("oss_code") or ""
|
||||||
|
pfx = base_prefix or "seed"
|
||||||
|
|
||||||
paths: List[str] = []
|
paths: List[str] = []
|
||||||
for ci, (cc, n) in enumerate(plan, start=1):
|
for ci, (cc, n) in enumerate(plan, start=1):
|
||||||
@@ -136,23 +139,25 @@ def seed_shot_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if not base or not Path(base).exists():
|
if not base or not Path(base).exists():
|
||||||
print(f"[seed_shot] {r.get('spu_code', '')} 参考图缺失({base}),跳过该颜色种草图")
|
print(f"[seed_shot] {r.get('spu_code', '')} 参考图缺失({base}),跳过该颜色种草图")
|
||||||
continue
|
continue
|
||||||
tag = _color_tag(cc, ci)
|
|
||||||
pfx = f"{base_prefix}_{tag}" if base_prefix else f"seed_{tag}"
|
|
||||||
generated = generate_seed_shots(ib, base, cn, material, n, str(shot_dir),
|
generated = generate_seed_shots(ib, base, cn, material, n, str(shot_dir),
|
||||||
r.get("composite_negative", ""),
|
r.get("composite_negative", ""),
|
||||||
size=size, prefix=pfx)
|
size=size, prefix=pfx, gender=gender)
|
||||||
paths.extend(generated)
|
paths.extend(generated)
|
||||||
if not paths:
|
if not paths:
|
||||||
return None
|
return None
|
||||||
r["seed_shot_paths"] = paths
|
r["seed_shot_paths"] = paths
|
||||||
urls: List[str] = []
|
urls: List[str] = []
|
||||||
for pth in paths:
|
for pth in paths:
|
||||||
|
# OSS key 用对应货号(img_code),不再自增;无货号时回退自增计数
|
||||||
with seq_lock:
|
with seq_lock:
|
||||||
|
if not base_prefix:
|
||||||
if seq >= MAX_CODE:
|
if seq >= MAX_CODE:
|
||||||
print(f"[seed_shot] 货号计数达上限 999,停止上传种草图")
|
print(f"[seed_shot] 货号计数达上限 999,停止上传种草图")
|
||||||
break
|
break
|
||||||
code = f"{prefix}{seq:03d}"
|
code = f"{prefix}{seq:03d}"
|
||||||
seq += 1
|
seq += 1
|
||||||
|
else:
|
||||||
|
code = base_prefix
|
||||||
if oss_enabled:
|
if oss_enabled:
|
||||||
try:
|
try:
|
||||||
compressed = compress_for_oss(pth, str(Path(pth).with_suffix(".oss.jpg")))
|
compressed = compress_for_oss(pth, str(Path(pth).with_suffix(".oss.jpg")))
|
||||||
|
|||||||
@@ -73,19 +73,18 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
db_path = root / db_path
|
db_path = root / db_path
|
||||||
break
|
break
|
||||||
|
|
||||||
from graph.template_export import export_product
|
from graph.template_export import export_products
|
||||||
tdir = (pcfg.get("template_dir") or "").strip() or str(Path(tp).parent)
|
tdir = (pcfg.get("template_dir") or "").strip() or str(Path(tp).parent)
|
||||||
prod_dir = output_dir / "product"
|
prod_dir = output_dir / "product"
|
||||||
prod_dir.mkdir(parents=True, exist_ok=True)
|
prod_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
exported: List[str] = []
|
# 批量合并导出:所有产品一次性写入同一模板,只打开/保存一次(避免逐产品频繁读写)
|
||||||
|
batch: List[Dict[str, Any]] = []
|
||||||
skipped = 0
|
skipped = 0
|
||||||
merged_out: Optional[str] = None # 合并模式:一次任务所有产品填同一个模板
|
|
||||||
is_first = True
|
|
||||||
for r in products:
|
for r in products:
|
||||||
# 失败跳过:合成图(composite/printed)与标题都失败的产品不写进模板
|
# 失败跳过:合成图(composite/printed)与标题都失败的产品不写进模板
|
||||||
has_img = bool(r.get("composite_path") or r.get("printed_path"))
|
has_img = bool(r.get("composite_path") or r.get("printed_path"))
|
||||||
has_title = bool((r.get("cn_title") or "").strip())
|
has_title = bool((r.get("en_title") or "").strip()) # 商品名称统一用 en_title
|
||||||
if not (has_img and has_title):
|
if not (has_img and has_title):
|
||||||
skipped += 1
|
skipped += 1
|
||||||
print(f"[template] 跳过失败产品 {r.get('spu_code')}/{r.get('img_code','')}: "
|
print(f"[template] 跳过失败产品 {r.get('spu_code')}/{r.get('img_code','')}: "
|
||||||
@@ -94,31 +93,37 @@ def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
sku_codes = [cc.get("sku_code") for cc in (r.get("color_composites") or [])]
|
sku_codes = [cc.get("sku_code") for cc in (r.get("color_composites") or [])]
|
||||||
if not sku_codes:
|
if not sku_codes:
|
||||||
sku_codes = [r.get("sku_code") or ""]
|
sku_codes = [r.get("sku_code") or ""]
|
||||||
|
batch.append({
|
||||||
|
"spu_code": r.get("spu_code", ""),
|
||||||
|
"sku_codes": sku_codes,
|
||||||
|
"images": [],
|
||||||
|
"spu_per_color": True, # 每颜色一个独立 SPU 块(单色多 SPU)
|
||||||
|
"oss_code": r.get("oss_code") or (r.get("color_composites") or [{}])[0].get("code", ""),
|
||||||
|
"cn_title": r.get("cn_title", ""),
|
||||||
|
"en_title": r.get("en_title", ""),
|
||||||
|
"ja_title": r.get("ja_title", ""),
|
||||||
|
"es_title": r.get("es_title", ""),
|
||||||
|
"composite_urls": r.get("color_composites") or [],
|
||||||
|
"seed_shot_urls": r.get("seed_shot_urls") or [],
|
||||||
|
})
|
||||||
|
|
||||||
|
exported: List[str] = []
|
||||||
|
if batch:
|
||||||
|
out = _template_out_path(prod_dir, "商品上传")
|
||||||
try:
|
try:
|
||||||
if is_first:
|
out = export_products(
|
||||||
merged_out = str(_template_out_path(prod_dir, "商品上传"))
|
db_path, batch, tdir, tp, str(out),
|
||||||
out = export_product(
|
|
||||||
db_path, r.get("spu_code", ""), sku_codes, tdir, tp,
|
|
||||||
merged_out,
|
|
||||||
images=[],
|
|
||||||
spu_per_color=True, # 每颜色一个独立 SPU 块(单色多 SPU)
|
|
||||||
oss_code=r.get("oss_code") or (r.get("color_composites") or [{}])[0].get("code", ""),
|
|
||||||
cn_title=r.get("cn_title", ""),
|
|
||||||
en_title=r.get("en_title", ""),
|
|
||||||
ja_title=r.get("ja_title", ""),
|
|
||||||
composite_urls=r.get("color_composites") or [],
|
|
||||||
seed_shot_urls=r.get("seed_shot_urls") or [],
|
|
||||||
append_to="" if is_first else merged_out, # 首个产品从模板创建,后续追加合并
|
|
||||||
markup_percent=float(pcfg.get("markup_percent") or 0),
|
markup_percent=float(pcfg.get("markup_percent") or 0),
|
||||||
)
|
)
|
||||||
|
for r in products:
|
||||||
|
if (r.get("composite_path") or r.get("printed_path")) and (r.get("en_title") or "").strip():
|
||||||
r["template_path"] = str(out)
|
r["template_path"] = str(out)
|
||||||
exported.append(str(out))
|
exported.append(str(out))
|
||||||
print(f"[template] 商品上传模板已生成({len(exported)}/{len(products)} 合并): {out}")
|
print(f"[template] 商品上传模板已生成({len(batch)} 个产品一次合并): {out}")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
errors.append({"node": "template_export", "type": type(e).__name__,
|
errors.append({"node": "template_export", "type": type(e).__name__,
|
||||||
"message": f"模板导出失败 {r.get('spu_code')}: {e}", "trace": ""})
|
"message": f"模板批量导出失败: {e}", "trace": ""})
|
||||||
print(f"[template] 模板导出失败 {r.get('spu_code')}: {e}")
|
print(f"[template] 模板批量导出失败: {e}")
|
||||||
is_first = False
|
|
||||||
|
|
||||||
stats["template_export"] = {"exported": len(exported)}
|
stats["template_export"] = {"exported": len(exported)}
|
||||||
return {"product": products, "errors": errors, "stats": stats}
|
return {"product": products, "errors": errors, "stats": stats}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"""Pinterest 参考模式共享辅助:种子词加载、已用搜索词持久化、搜索词全局过滤。
|
"""Pinterest 参考模式共享辅助:种子词加载、已用搜索词持久化、搜索词全局过滤、设计图全局 MD5 去重。
|
||||||
|
|
||||||
独立于 Google Trends 采集链路,供 pinterest_search / scrape / analyze 节点复用。
|
独立于 Google Trends 采集链路,供 pinterest_search / scrape / analyze 节点复用。
|
||||||
"""
|
"""
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
@@ -114,3 +117,223 @@ def merge_used(existing: List[str], new_terms: List[str]) -> List[str]:
|
|||||||
seen.add(k)
|
seen.add(k)
|
||||||
out.append(str(t).strip())
|
out.append(str(t).strip())
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# —— 设计图全局 MD5 去重(跨国家、跨运行;对所有国家生效)——
|
||||||
|
_MD5_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def global_md5_path() -> Path:
|
||||||
|
"""全局设计图 MD5 过滤文件(.cache 随打包同步,跨版本保留)。"""
|
||||||
|
return runtime_root() / ".cache" / "global_design_md5.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_global_md5() -> set:
|
||||||
|
"""读全局设计图 MD5 集合。"""
|
||||||
|
try:
|
||||||
|
p = global_md5_path()
|
||||||
|
if p.exists():
|
||||||
|
data = json.loads(p.read_text(encoding="utf-8"))
|
||||||
|
return {str(m).strip().lower() for m in (data.get("md5s") or []) if str(m).strip()}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 全局 MD5 过滤读取失败: {e}")
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def add_global_md5(md5: str) -> bool:
|
||||||
|
"""把设计图 MD5 加入全局过滤;返回 True=新增可用,False=已存在(全局重复,应跳过)。"""
|
||||||
|
md5 = str(md5 or "").strip().lower()
|
||||||
|
if not md5:
|
||||||
|
return False
|
||||||
|
with _MD5_LOCK:
|
||||||
|
try:
|
||||||
|
p = global_md5_path()
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
md5s = load_global_md5()
|
||||||
|
if md5 in md5s:
|
||||||
|
return False
|
||||||
|
md5s.add(md5)
|
||||||
|
p.write_text(json.dumps(
|
||||||
|
{"updated_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "md5s": sorted(md5s)},
|
||||||
|
ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return True
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 全局 MD5 过滤写入失败: {e}")
|
||||||
|
return True # 写入失败不阻塞:按新增处理,避免误跳过
|
||||||
|
|
||||||
|
|
||||||
|
def design_md5_ok(image_path: str) -> bool:
|
||||||
|
"""计算设计图 MD5 并加入全局过滤;返回 True=新增可用,False=全局重复(应跳过)。"""
|
||||||
|
try:
|
||||||
|
md5 = hashlib.md5(Path(image_path).read_bytes()).hexdigest()
|
||||||
|
return add_global_md5(md5)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 设计图 MD5 计算失败: {e}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# —— 图池(Image Pool):跨轮次/跨运行持久化的爬取图片池 + 已消费图片 MD5 拉黑 ——
|
||||||
|
# 图池 = 所有已爬取图片的注册表(path + md5 + term);已消费(分析过)的图片 MD5 记入
|
||||||
|
# used_images,不再复用。分析从图池取未消费图片,池不足时由路由触发新一轮搜索。
|
||||||
|
|
||||||
|
_IMG_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp")
|
||||||
|
|
||||||
|
|
||||||
|
def image_pool_path(output_dir: str, country: str) -> Path:
|
||||||
|
return Path(output_dir) / "pinterest_ref" / country / "image_pool.json"
|
||||||
|
|
||||||
|
|
||||||
|
def used_images_path(output_dir: str, country: str) -> Path:
|
||||||
|
return Path(output_dir) / "pinterest_ref" / country / "used_images.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_image_pool(output_dir: str, country: str) -> Dict[str, Any]:
|
||||||
|
"""读图池注册表;无文件时回退扫描文件系统重建。"""
|
||||||
|
try:
|
||||||
|
p = image_pool_path(output_dir, country)
|
||||||
|
if p.exists():
|
||||||
|
data = json.loads(p.read_text(encoding="utf-8")) or {}
|
||||||
|
imgs = data.get("images") or []
|
||||||
|
if imgs:
|
||||||
|
return {"updated_at": data.get("updated_at", ""), "images": imgs}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 图池读取失败: {e}")
|
||||||
|
return {"updated_at": "", "images": scan_scraped_images(output_dir, country)}
|
||||||
|
|
||||||
|
|
||||||
|
def save_image_pool(output_dir: str, country: str, pool: Dict[str, Any]) -> None:
|
||||||
|
"""持久化图池注册表。"""
|
||||||
|
try:
|
||||||
|
p = image_pool_path(output_dir, country)
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(json.dumps(
|
||||||
|
{"updated_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "images": pool.get("images") or []},
|
||||||
|
ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 图池保存失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def scan_scraped_images(output_dir: str, country: str) -> List[Dict[str, Any]]:
|
||||||
|
"""扫描 pinterest_ref/<country>/*/ 下所有图片,重建图池注册表(含 md5)。"""
|
||||||
|
base = Path(output_dir) / "pinterest_ref" / country
|
||||||
|
out: List[Dict[str, Any]] = []
|
||||||
|
if not base.exists():
|
||||||
|
return out
|
||||||
|
for term_dir in sorted(base.iterdir()):
|
||||||
|
if not term_dir.is_dir():
|
||||||
|
continue
|
||||||
|
term = term_dir.name
|
||||||
|
for f in sorted(term_dir.iterdir()):
|
||||||
|
if not f.is_file() or f.suffix.lower() not in _IMG_EXTS:
|
||||||
|
continue
|
||||||
|
out.append({"path": str(f), "md5": image_md5(str(f)), "term": term})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def image_md5(path: str) -> str:
|
||||||
|
"""计算图片文件 MD5(失败返回空串)。"""
|
||||||
|
try:
|
||||||
|
return hashlib.md5(Path(path).read_bytes()).hexdigest()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 图片 MD5 计算失败 {path}: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def load_used_images(output_dir: str, country: str) -> set:
|
||||||
|
"""读已消费图片 MD5 集合(拉黑,不再复用)。"""
|
||||||
|
try:
|
||||||
|
p = used_images_path(output_dir, country)
|
||||||
|
if p.exists():
|
||||||
|
data = json.loads(p.read_text(encoding="utf-8")) or {}
|
||||||
|
return {str(m).strip().lower() for m in (data.get("md5s") or []) if str(m).strip()}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 已消费图片读取失败: {e}")
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def save_used_images(output_dir: str, country: str, md5s: set) -> None:
|
||||||
|
"""持久化已消费图片 MD5 集合。"""
|
||||||
|
try:
|
||||||
|
p = used_images_path(output_dir, country)
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(json.dumps(
|
||||||
|
{"updated_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "md5s": sorted(md5s)},
|
||||||
|
ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 已消费图片保存失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def pool_unused_images(pool: Dict[str, Any], used: set) -> List[Dict[str, Any]]:
|
||||||
|
"""从图池取未消费图片(md5 不在 used 集合),保序。"""
|
||||||
|
out = []
|
||||||
|
for img in pool.get("images") or []:
|
||||||
|
if not isinstance(img, dict):
|
||||||
|
continue
|
||||||
|
m = str(img.get("md5") or "").strip().lower()
|
||||||
|
if not m or m in used:
|
||||||
|
continue
|
||||||
|
if not Path(str(img.get("path") or "")).exists():
|
||||||
|
continue
|
||||||
|
out.append(img)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def compress_image(path: str, max_dim: int = 1024, max_bytes: int = 1_500_000,
|
||||||
|
out_dir: str = "") -> str:
|
||||||
|
"""压缩大图:超过 max_dim 边长或 max_bytes 体积时缩放/重编码,返回压缩后路径。
|
||||||
|
|
||||||
|
- 内存占用过大(分辨率过高)→ 等比缩放到 max_dim 内;
|
||||||
|
- 文件过大 → 转 JPEG 重编码(质量自适应);
|
||||||
|
- 无需压缩 → 返回原路径。压缩产物存 out_dir(默认图片同目录 .compressed/)。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
p = Path(path)
|
||||||
|
if not p.exists():
|
||||||
|
return path
|
||||||
|
size = p.stat().st_size
|
||||||
|
try:
|
||||||
|
with Image.open(p) as im:
|
||||||
|
w, h = im.size
|
||||||
|
except Exception: # noqa: BLE001 无法解析的图片(损坏/非标准)直接返回原路径
|
||||||
|
return path
|
||||||
|
if w <= max_dim and h <= max_dim and size <= max_bytes:
|
||||||
|
return path
|
||||||
|
out_root = Path(out_dir) if out_dir else (p.parent / ".compressed")
|
||||||
|
out_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
out = out_root / f"{p.stem}_c.jpg"
|
||||||
|
with Image.open(p) as im:
|
||||||
|
im = im.convert("RGB")
|
||||||
|
im.thumbnail((max_dim, max_dim), Image.LANCZOS)
|
||||||
|
quality = 85
|
||||||
|
while quality >= 40:
|
||||||
|
im.save(out, "JPEG", quality=quality, optimize=True)
|
||||||
|
if out.stat().st_size <= max_bytes:
|
||||||
|
break
|
||||||
|
quality -= 15
|
||||||
|
print(f"[pinterest] 图片压缩 {p.name} ({w}x{h}, {size // 1024}KB) → "
|
||||||
|
f"{out.name} ({out.stat().st_size // 1024}KB)")
|
||||||
|
return str(out)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest] 图片压缩失败 {path}: {e}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def is_400_content_image(exc) -> bool:
|
||||||
|
"""HTTP 400 且错误信息模糊匹配到「内容」或「图片」才算(用户要求)。
|
||||||
|
|
||||||
|
生图后端抛 RuntimeError("图像 API 400: {body}"),body 在消息里;
|
||||||
|
多模态后端抛 requests.HTTPError,响应体在 exc.response.text。
|
||||||
|
"""
|
||||||
|
msg = str(exc or "")
|
||||||
|
resp = getattr(exc, "response", None)
|
||||||
|
if resp is not None:
|
||||||
|
try:
|
||||||
|
body = resp.text or ""
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
body = ""
|
||||||
|
if body:
|
||||||
|
msg = f"{msg} {body}"
|
||||||
|
if "400" not in msg:
|
||||||
|
return False
|
||||||
|
return ("内容" in msg or "图片" in msg)
|
||||||
|
|||||||
@@ -0,0 +1,589 @@
|
|||||||
|
"""Pinterest 简报池 + 并发生成流水线。
|
||||||
|
|
||||||
|
分析节点产出简报后立即推入简报池,后台 worker 逐条并发生成:
|
||||||
|
生成设计(compose) → 三合一(product) → OSS上传 → 生成种草图(seed_shot)
|
||||||
|
不等全部分析完,边分析边生成,显著缩短总耗时。
|
||||||
|
|
||||||
|
集成:
|
||||||
|
pinterest_init 创建 PinterestPipeline(存 state["pinterest_pipeline"])
|
||||||
|
pinterest_analyze 每批产出简报 → pipe.add_briefs(new_briefs)
|
||||||
|
pinterest_finalize → pipe.finish()(排空 + 合并产品 + 补写报告)→ template_export
|
||||||
|
|
||||||
|
并发上限与 product_node 一致(默认 5),避免压垮图像网关。
|
||||||
|
"""
|
||||||
|
import concurrent.futures
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from graph.paths import project_root, runtime_root
|
||||||
|
|
||||||
|
|
||||||
|
class PinterestPipeline:
|
||||||
|
def __init__(self, state: Dict[str, Any]):
|
||||||
|
self.config = state["config"]
|
||||||
|
self.country = state.get("country", "")
|
||||||
|
self.output_dir = Path(state["output_dir"])
|
||||||
|
self.cache_dir = Path(state.get("cache_dir") or self.output_dir)
|
||||||
|
self.task_timestamp = str(state.get("task_timestamp") or time.strftime("%Y%m%d%H%M%S"))
|
||||||
|
|
||||||
|
# 简报池 + 信号
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._cond = threading.Condition(self._lock)
|
||||||
|
self._briefs: List[Dict[str, Any]] = []
|
||||||
|
self._done = False
|
||||||
|
self._cursor = 0
|
||||||
|
|
||||||
|
# 结果
|
||||||
|
self._products: List[Dict[str, Any]] = []
|
||||||
|
self._products_lock = threading.Lock()
|
||||||
|
self._errors: List[Dict[str, Any]] = []
|
||||||
|
self._errors_lock = threading.Lock()
|
||||||
|
|
||||||
|
# 路径解析(复用 product_node 的 _abs 逻辑:运行根优先,其次数据根)
|
||||||
|
pcfg = self.config.get("product") or {}
|
||||||
|
|
||||||
|
def _abs(key: str, default: str) -> Path:
|
||||||
|
p = Path(pcfg.get(key, default))
|
||||||
|
if p.is_absolute():
|
||||||
|
return p
|
||||||
|
for root in (runtime_root(), project_root()):
|
||||||
|
cand = root / p
|
||||||
|
if cand.exists():
|
||||||
|
return cand
|
||||||
|
return project_root() / p
|
||||||
|
|
||||||
|
self._db_path = _abs("db_path", "db/spu_sku.db")
|
||||||
|
self._basemap_root = _abs("basemap_dir", "basemap")
|
||||||
|
self._material_root = _abs("material_library_dir", "material_library")
|
||||||
|
self._category = pcfg.get("model_category", "T-shirt")
|
||||||
|
self._prefix = str(pcfg.get("code_prefix") or "DG").strip()
|
||||||
|
|
||||||
|
# 任务清单(spu_tasks → [(spu, skus)],简报按序号绑定)
|
||||||
|
self._worklist = self._build_worklist()
|
||||||
|
|
||||||
|
# 图像后端(compose + product 共用)/ 标题后端
|
||||||
|
self._ib = self._init_image_backend()
|
||||||
|
self._title_backend = self._init_title_backend()
|
||||||
|
# 分析后端(失败/侵权时从图池补充图片重新分析用)
|
||||||
|
self._analyze_backend = self._init_analyze_backend()
|
||||||
|
self._country_config = state.get("country_config") or {}
|
||||||
|
# 补充重试次数:设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数
|
||||||
|
self._supply_attempts = int((self.config.get("pinterest") or {}).get("supply_attempts", 3))
|
||||||
|
|
||||||
|
# 400 计数(per 种子词):多模态 + 生图模型合计,超限放弃当前种子词
|
||||||
|
self._err400_lock = threading.Lock()
|
||||||
|
self._err400_count = 0
|
||||||
|
self._err400_limit = int((self.config.get("pinterest") or {}).get("err400_limit", 15))
|
||||||
|
self._err400_aborted = False
|
||||||
|
self._err400_term = ""
|
||||||
|
|
||||||
|
# 模特分配(一个 SPU 一个模特,SPU>模特数循环兜底)
|
||||||
|
self._model_assign = self._assign_models()
|
||||||
|
|
||||||
|
# 材质映射(seed_shot 用)
|
||||||
|
self._material_map = self._load_materials()
|
||||||
|
|
||||||
|
# 类目 → 性别(seed_shot 用):模版「类目」表头值含「男」→ 男模;含「女」→ 女模;都不含 → 全部随机
|
||||||
|
self._gender = None
|
||||||
|
tp = str((pcfg.get("template_path") or "") or "").strip()
|
||||||
|
if tp:
|
||||||
|
from graph.seed_shot import read_template_category, gender_from_category
|
||||||
|
category = read_template_category(tp)
|
||||||
|
self._gender = gender_from_category(category)
|
||||||
|
if self._gender:
|
||||||
|
print(f"[pinterest_pipeline] 类目「{category[:30]}…」含{'男' if self._gender == 'male' else '女'} → 固定 {self._gender} 模特")
|
||||||
|
elif category:
|
||||||
|
print(f"[pinterest_pipeline] 类目「{category[:30]}…」无男/女 → 男女模特随机")
|
||||||
|
|
||||||
|
# OSS
|
||||||
|
self._oss_cfg = self.config.get("oss") or {}
|
||||||
|
self._oss_enabled = bool(self._oss_cfg.get("enabled", True)) and bool(
|
||||||
|
self._oss_cfg.get("oss_bucket"))
|
||||||
|
self.oss_seq = int(state.get("oss_seq") or 0)
|
||||||
|
self._oss_lock = threading.Lock()
|
||||||
|
|
||||||
|
# 并发线程池(默认 5,与 product_node 上限一致)
|
||||||
|
concurrency = int(pcfg.get("concurrency") or 0) or 5
|
||||||
|
self._pool = concurrent.futures.ThreadPoolExecutor(max_workers=concurrency)
|
||||||
|
|
||||||
|
# 分发线程:拉简报 → 逐条提交线程池
|
||||||
|
self._dispatcher = threading.Thread(target=self._dispatch, daemon=True)
|
||||||
|
self._dispatcher.start()
|
||||||
|
print(f"[pinterest_pipeline] 简报池启动:{len(self._worklist)} 个产品任务,"
|
||||||
|
f"并发 {concurrency}({self.country})")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 初始化辅助
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _build_worklist(self) -> List[tuple]:
|
||||||
|
pcfg = self.config.get("product") or {}
|
||||||
|
spu_tasks = pcfg.get("spu_tasks") or []
|
||||||
|
worklist: List[tuple] = []
|
||||||
|
if not spu_tasks:
|
||||||
|
return worklist
|
||||||
|
try:
|
||||||
|
from graph.product import list_spus
|
||||||
|
spus = list_spus(str(self._db_path))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] SPU 读取失败: {e}")
|
||||||
|
spus = []
|
||||||
|
for t in spu_tasks:
|
||||||
|
code = (t.get("spu") or t.get("spu_code") or "").strip()
|
||||||
|
spu = next((s for s in spus if s["code"] == code), None)
|
||||||
|
if spu is None:
|
||||||
|
print(f"[pinterest_pipeline] 任务款号 {code} 不在 db,跳过")
|
||||||
|
continue
|
||||||
|
worklist.append((spu, (t.get("skus") or "").strip()))
|
||||||
|
return worklist
|
||||||
|
|
||||||
|
def _init_image_backend(self):
|
||||||
|
compose_cfg = self.config.get("compose") or {}
|
||||||
|
backend_name = (compose_cfg.get("backend") or "").strip()
|
||||||
|
if not backend_name:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from graph.backends import get_image_backend
|
||||||
|
ib = get_image_backend(backend_name)
|
||||||
|
if ib is not None:
|
||||||
|
ib.bind_config(compose_cfg)
|
||||||
|
return ib
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] 图像后端不可用: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _init_title_backend(self):
|
||||||
|
ls_cfg = self.config.get("llm_screen") or {}
|
||||||
|
if (ls_cfg.get("provider") or "") in ("", "mock"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from graph.llms import get_backend as _glb
|
||||||
|
tb = _glb(ls_cfg.get("provider"))
|
||||||
|
tb.bind_config(ls_cfg)
|
||||||
|
if getattr(tb, "has_key", False):
|
||||||
|
return tb
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] 标题后端初始化失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _init_analyze_backend(self):
|
||||||
|
"""图片分析后端(失败/侵权时从图池补充图片重新分析用)。"""
|
||||||
|
pcfg = self.config.get("pinterest") or {}
|
||||||
|
provider = str(pcfg.get("provider") or "openai").strip().lower()
|
||||||
|
if provider == "static":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from graph.llms import get_backend
|
||||||
|
llm = get_backend(provider)
|
||||||
|
if hasattr(llm, "bind_config"):
|
||||||
|
llm.bind_config(self.config.get("llm_screen") or {})
|
||||||
|
if provider not in ("mock",) and not getattr(llm, "has_key", False):
|
||||||
|
print(f"[pinterest_pipeline] {provider} 未配置 API key,降级 mock")
|
||||||
|
llm = get_backend("mock")
|
||||||
|
return llm
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] 分析后端初始化失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _assign_models(self) -> Dict[str, Any]:
|
||||||
|
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)
|
||||||
|
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]
|
||||||
|
return model_assign
|
||||||
|
|
||||||
|
def _load_materials(self) -> Dict[str, str]:
|
||||||
|
material_map: Dict[str, str] = {}
|
||||||
|
try:
|
||||||
|
from graph.product import list_spus
|
||||||
|
for s in list_spus(str(self._db_path)):
|
||||||
|
m = " ".join(str(s.get("material", "")).replace("\r", " ").replace("\n", " ").split())
|
||||||
|
material_map[s["code"]] = m
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] 材质读取失败(用空): {e}")
|
||||||
|
return material_map
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 对外接口
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def reset_400(self, term: str = "") -> None:
|
||||||
|
"""新一批图爬取完成后调用:重置 400 计数并记录当前种子词。"""
|
||||||
|
with self._err400_lock:
|
||||||
|
self._err400_count = 0
|
||||||
|
self._err400_aborted = False
|
||||||
|
self._err400_term = term or ""
|
||||||
|
|
||||||
|
def record_400(self) -> bool:
|
||||||
|
"""记录一次 400(含内容/图片)。返回 True 表示本次触发放弃当前种子词。"""
|
||||||
|
with self._err400_lock:
|
||||||
|
self._err400_count += 1
|
||||||
|
if self._err400_count > self._err400_limit and not self._err400_aborted:
|
||||||
|
self._err400_aborted = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_400_aborted(self) -> bool:
|
||||||
|
with self._err400_lock:
|
||||||
|
return self._err400_aborted
|
||||||
|
|
||||||
|
def _abort_current_term(self) -> None:
|
||||||
|
"""放弃当前种子词:清空其未完成简报 + 图池未消费图片(已完成的保留)。"""
|
||||||
|
term = self._err400_term
|
||||||
|
with self._cond:
|
||||||
|
kept = [b for b in self._briefs if not self._brief_of_term(b, term)]
|
||||||
|
dropped = len(self._briefs) - len(kept)
|
||||||
|
self._briefs = kept
|
||||||
|
if dropped:
|
||||||
|
print(f"[pinterest_pipeline] 放弃「{term}」未完成简报 {dropped} 条")
|
||||||
|
self._drop_term_pool(term)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _brief_of_term(b: Dict[str, Any], term: str) -> bool:
|
||||||
|
"""简报是否属于某种子词(topic 去掉 #N 后缀后 == term)。"""
|
||||||
|
if not term:
|
||||||
|
return False
|
||||||
|
topic = str(b.get("topic") or "").strip()
|
||||||
|
base = re.sub(r"\s+#\d+$", "", topic).strip().lower()
|
||||||
|
return bool(base) and base == term.strip().lower()
|
||||||
|
|
||||||
|
def _drop_term_pool(self, term: str) -> None:
|
||||||
|
"""清除图池中属于当前种子词的图片,并把它们的 md5 全部拉黑(used_images.json)。
|
||||||
|
|
||||||
|
400 超限说明这批图反复触发内容/图片 400,整批拉黑防止下次重新爬取到相同图再次触发。
|
||||||
|
"""
|
||||||
|
if not term:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from graph.pinterest import (
|
||||||
|
load_image_pool, save_image_pool,
|
||||||
|
load_used_images, save_used_images,
|
||||||
|
)
|
||||||
|
pool = load_image_pool(str(self.output_dir), self.country)
|
||||||
|
imgs = pool.get("images") or []
|
||||||
|
term_imgs = [img for img in imgs
|
||||||
|
if str(img.get("term") or "").strip().lower() == term.strip().lower()]
|
||||||
|
kept = [img for img in imgs if img not in term_imgs]
|
||||||
|
if len(kept) < len(imgs):
|
||||||
|
pool["images"] = kept
|
||||||
|
save_image_pool(str(self.output_dir), self.country, pool)
|
||||||
|
print(f"[pinterest_pipeline] 清除图池「{term}」图片 {len(imgs) - len(kept)} 张")
|
||||||
|
md5s = [str(img.get("md5") or "").strip().lower() for img in term_imgs]
|
||||||
|
md5s = [m for m in md5s if m]
|
||||||
|
if md5s:
|
||||||
|
used = load_used_images(str(self.output_dir), self.country)
|
||||||
|
before = len(used)
|
||||||
|
used.update(md5s)
|
||||||
|
if len(used) > before:
|
||||||
|
save_used_images(str(self.output_dir), self.country, used)
|
||||||
|
print(f"[pinterest_pipeline] 400 超限:拉黑「{term}」图片 md5 {len(md5s)} 个")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] 清除图池失败: {e}")
|
||||||
|
|
||||||
|
def add_briefs(self, briefs: List[Dict[str, Any]]) -> None:
|
||||||
|
if not briefs:
|
||||||
|
return
|
||||||
|
with self._cond:
|
||||||
|
self._briefs.extend(briefs)
|
||||||
|
self._cond.notify_all()
|
||||||
|
print(f"[pinterest_pipeline] 简报池 +{len(briefs)} 条(待处理 {len(self._briefs)})")
|
||||||
|
|
||||||
|
def finish(self) -> tuple:
|
||||||
|
"""排空简报池、等待全部产品完成,返回 (products, errors)。"""
|
||||||
|
with self._cond:
|
||||||
|
self._done = True
|
||||||
|
self._cond.notify_all()
|
||||||
|
self._dispatcher.join()
|
||||||
|
self._pool.shutdown(wait=True)
|
||||||
|
with self._products_lock:
|
||||||
|
products = list(self._products)
|
||||||
|
with self._errors_lock:
|
||||||
|
errors = list(self._errors)
|
||||||
|
print(f"[pinterest_pipeline] 收尾:完成 {len(products)} 个产品,错误 {len(errors)}")
|
||||||
|
return products, errors
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 后台线程
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _dispatch(self) -> None:
|
||||||
|
while True:
|
||||||
|
with self._cond:
|
||||||
|
while not self._briefs and not self._done:
|
||||||
|
self._cond.wait()
|
||||||
|
if self._done and not self._briefs:
|
||||||
|
break
|
||||||
|
batch = self._briefs
|
||||||
|
self._briefs = []
|
||||||
|
for b in batch:
|
||||||
|
with self._cond:
|
||||||
|
idx = self._cursor
|
||||||
|
self._cursor += 1
|
||||||
|
self._pool.submit(self._process_one, b, idx)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 单条简报完整链路:设计 → 三合一 → OSS → 种草图
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _process_one(self, brief: Dict[str, Any], idx: int) -> None:
|
||||||
|
try:
|
||||||
|
# 0) 先定货号:整条链路(设计/三合一/种草图)都用它命名与匹配,避免序号错位
|
||||||
|
if idx >= len(self._worklist):
|
||||||
|
print(f"[pinterest_pipeline] 简报 {idx} 无对应产品任务,跳过")
|
||||||
|
return
|
||||||
|
spu, skus = self._worklist[idx]
|
||||||
|
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_400_aborted():
|
||||||
|
# 当前种子词 400 超限已放弃:正在生成的当个也放弃,不进入后续链路
|
||||||
|
print(f"[pinterest_pipeline] 当前种子词 400 超限已放弃,跳过简报 {idx}")
|
||||||
|
return
|
||||||
|
if not design_path:
|
||||||
|
# 生成失败/侵权(MD5 全局重复/API 错误)→ 从图池补充图片重新分析,最多尝试 N 次;
|
||||||
|
# 图池不足 → 返回 None,由路由在下一轮触发搜索
|
||||||
|
for _ in range(self._supply_attempts):
|
||||||
|
new_brief = self._supply_from_pool(
|
||||||
|
reason=f"简报「{brief.get('topic','')}」设计生成失败")
|
||||||
|
if new_brief is None:
|
||||||
|
return
|
||||||
|
brief = new_brief
|
||||||
|
design_path = self._gen_design(brief, img_code)
|
||||||
|
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)
|
||||||
|
if not prod:
|
||||||
|
return
|
||||||
|
# 3) OSS 上传
|
||||||
|
self._upload_product(prod)
|
||||||
|
# 4) 种草图——同一货号
|
||||||
|
self._seed_shot(prod)
|
||||||
|
# 去重记录
|
||||||
|
try:
|
||||||
|
from graph.nodes.product_node import _record_used
|
||||||
|
_record_used(self.cache_dir, prod)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
with self._products_lock:
|
||||||
|
self._products.append(prod)
|
||||||
|
print(f"[pinterest_pipeline] 产品完成: {prod.get('img_code', '')}"
|
||||||
|
f"(累计 {len(self._products)})")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
with self._errors_lock:
|
||||||
|
self._errors.append({"node": "pinterest_pipeline", "type": type(e).__name__,
|
||||||
|
"message": f"简报 {idx} 处理失败: {e}", "trace": ""})
|
||||||
|
print(f"[pinterest_pipeline] 简报 {idx} 处理失败: {e}")
|
||||||
|
|
||||||
|
def _gen_design(self, brief: Dict[str, Any], img_code: str) -> Optional[str]:
|
||||||
|
if self._ib is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from graph.nodes.compose_node import generate_design
|
||||||
|
design_dir = self.output_dir / "designs"
|
||||||
|
design_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _on_400():
|
||||||
|
if self.record_400():
|
||||||
|
self._abort_current_term()
|
||||||
|
|
||||||
|
return generate_design(self._ib, brief, design_dir, img_code, self._errors,
|
||||||
|
on_400=_on_400,
|
||||||
|
size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024"))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
with self._errors_lock:
|
||||||
|
self._errors.append({"node": "compose", "type": type(e).__name__,
|
||||||
|
"message": f"设计稿生成失败 {brief.get('topic', '')}: {e}",
|
||||||
|
"trace": ""})
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _supply_from_pool(self, reason: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""生成失败/侵权时,从图池取一张未消费图片重新分析,产出新简报。
|
||||||
|
|
||||||
|
图池不足 → 返回 None(由路由在下一轮触发搜索)。该图片分析后 md5 一律拉黑
|
||||||
|
(合适/不合适都拉黑),避免重复分析。
|
||||||
|
"""
|
||||||
|
from graph.pinterest import (
|
||||||
|
compress_image,
|
||||||
|
load_image_pool,
|
||||||
|
load_used_images,
|
||||||
|
pool_unused_images,
|
||||||
|
save_used_images,
|
||||||
|
)
|
||||||
|
pool = load_image_pool(str(self.output_dir), self.country)
|
||||||
|
used = load_used_images(str(self.output_dir), self.country)
|
||||||
|
unused = pool_unused_images(pool, used)
|
||||||
|
if not unused:
|
||||||
|
print(f"[pinterest_pipeline] 图池无未消费图片,无法补充({reason}),等待路由搜索")
|
||||||
|
return None
|
||||||
|
img = unused[0]
|
||||||
|
compressed = compress_image(img["path"])
|
||||||
|
llm = self._analyze_backend
|
||||||
|
if llm is None or not hasattr(llm, "analyze_pinterest_images"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
def _on_400():
|
||||||
|
if self.record_400():
|
||||||
|
self._abort_current_term()
|
||||||
|
res = llm.analyze_pinterest_images([compressed], img.get("term", ""), self.country,
|
||||||
|
on_400=_on_400) or []
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] 补充分析失败: {e}")
|
||||||
|
res = []
|
||||||
|
# 该图片已消费 → 拉黑(合适/不合适都拉黑)
|
||||||
|
if img.get("md5"):
|
||||||
|
used.add(str(img["md5"]).lower())
|
||||||
|
save_used_images(str(self.output_dir), self.country, used)
|
||||||
|
if not res or not isinstance(res[0], dict):
|
||||||
|
return None
|
||||||
|
b = res[0]
|
||||||
|
from graph.nodes.pinterest_analyze_node import _brief_suitable
|
||||||
|
if not _brief_suitable(b):
|
||||||
|
print(f"[pinterest_pipeline] 补充简报侵权/不适合印花,丢弃: {b.get('topic','')}")
|
||||||
|
return None
|
||||||
|
b["ref_images"] = [img["path"]]
|
||||||
|
b["source_md5"] = str(img.get("md5") or "").strip().lower()
|
||||||
|
try:
|
||||||
|
from graph.nodes.pinterest_analyze_node import _enrich_briefs
|
||||||
|
from graph.nodes.prompt_node import prompt_node
|
||||||
|
screened = _enrich_briefs([b], self.country)
|
||||||
|
if not screened:
|
||||||
|
return None
|
||||||
|
r = prompt_node({
|
||||||
|
"config": self.config, "country": self.country,
|
||||||
|
"country_config": self._country_config, "screened": screened,
|
||||||
|
})
|
||||||
|
new_briefs = r.get("briefs") or []
|
||||||
|
if new_briefs:
|
||||||
|
print(f"[pinterest_pipeline] 图池补充成功({reason}): {new_briefs[0].get('topic','')}")
|
||||||
|
return new_briefs[0]
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] 补充简报装配失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _process_spu(self, brief: Dict[str, Any], spu, skus: str, img_code: str,
|
||||||
|
design_path: str) -> 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)
|
||||||
|
# 设计稿已按货号命名(designs/{img_code}_design.png),直接复用,无需再拷贝
|
||||||
|
brief = dict(brief)
|
||||||
|
brief["design_path"] = design_path
|
||||||
|
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", "")))
|
||||||
|
if r:
|
||||||
|
r["img_code"] = img_code
|
||||||
|
return r
|
||||||
|
|
||||||
|
def _upload_product(self, r: Dict[str, Any]) -> None:
|
||||||
|
if not self._oss_enabled:
|
||||||
|
return
|
||||||
|
from graph.oss_upload import build_oss_key, compress_for_oss, upload_to_oss
|
||||||
|
from graph.nodes.oss_upload_node import KIND_ORDER, _gen_rand4
|
||||||
|
# 货号直接取产品 img_code:同一货号的所有图片(合成/平铺/底图)共用同一货号,
|
||||||
|
# 避免独立计数器在有产品被跳过时与 img_code 错位
|
||||||
|
base_code = r.get("img_code") or r.get("oss_code") or ""
|
||||||
|
if not base_code:
|
||||||
|
return
|
||||||
|
for kind in KIND_ORDER:
|
||||||
|
src = r.get(f"{kind}_path")
|
||||||
|
if not src or not Path(src).exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
compressed = compress_for_oss(src, str(Path(src).with_suffix(".oss.jpg")))
|
||||||
|
key = build_oss_key(self.country, self.task_timestamp, base_code, _gen_rand4())
|
||||||
|
url = upload_to_oss(self._oss_cfg, compressed, key)
|
||||||
|
if url:
|
||||||
|
r[f"{kind}_url"] = url
|
||||||
|
r["oss_code"] = base_code
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] OSS 上传失败 {src}: {e}")
|
||||||
|
# 多色:首色用主图 url/code,额外色单独上传(同一货号)
|
||||||
|
comps = r.get("color_composites") or []
|
||||||
|
if comps and r.get("composite_url"):
|
||||||
|
comps[0]["url"] = r["composite_url"]
|
||||||
|
comps[0]["code"] = r.get("oss_code", "")
|
||||||
|
for cc in comps[1:]:
|
||||||
|
src = cc.get("composite_path")
|
||||||
|
if not src or not Path(src).exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
compressed = compress_for_oss(src, str(Path(src).with_suffix(".oss.jpg")))
|
||||||
|
key = build_oss_key(self.country, self.task_timestamp, base_code, _gen_rand4())
|
||||||
|
url = upload_to_oss(self._oss_cfg, compressed, key)
|
||||||
|
if url:
|
||||||
|
cc["url"] = url
|
||||||
|
cc["code"] = base_code
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] OSS 颜色图上传失败 {src}: {e}")
|
||||||
|
|
||||||
|
def _seed_shot(self, r: Dict[str, Any]) -> None:
|
||||||
|
ss_cfg = self.config.get("seed_shot") or {}
|
||||||
|
count = int(ss_cfg.get("count", 1))
|
||||||
|
if count <= 0 or not bool(ss_cfg.get("enabled", True)) or self._ib is None:
|
||||||
|
return
|
||||||
|
from graph.nodes.seed_shot_node import _plan_seed_shots
|
||||||
|
from graph.seed_shot import generate_seed_shots
|
||||||
|
from graph.oss_upload import build_oss_key, compress_for_oss, upload_to_oss
|
||||||
|
from graph.nodes.oss_upload_node import MAX_CODE, _gen_rand4
|
||||||
|
comps = r.get("color_composites") or []
|
||||||
|
if not comps and r.get("composite_path") and Path(r["composite_path"]).exists():
|
||||||
|
comps = [{"sku_code": r.get("sku_code"), "color": r.get("color", ""),
|
||||||
|
"composite_path": r["composite_path"]}]
|
||||||
|
if not comps:
|
||||||
|
return
|
||||||
|
plan = _plan_seed_shots(comps, count)
|
||||||
|
cn = (r.get("cn_title") or "").strip() or r.get("topic", "")
|
||||||
|
material = self._material_map.get(r.get("spu_code", ""), "")
|
||||||
|
base_prefix = r.get("img_code") or r.get("oss_code") or ""
|
||||||
|
pfx = base_prefix or "seed"
|
||||||
|
shot_dir = self.output_dir / "seed_shots"
|
||||||
|
shot_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
size = str(ss_cfg.get("size") or "1536x2048")
|
||||||
|
paths: List[str] = []
|
||||||
|
for cc, n in plan:
|
||||||
|
base = cc.get("composite_path")
|
||||||
|
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)
|
||||||
|
paths.extend(generated)
|
||||||
|
if not paths:
|
||||||
|
return
|
||||||
|
r["seed_shot_paths"] = paths
|
||||||
|
urls: List[str] = []
|
||||||
|
for pth in paths:
|
||||||
|
with self._oss_lock:
|
||||||
|
if not base_prefix:
|
||||||
|
if self.oss_seq >= MAX_CODE:
|
||||||
|
break
|
||||||
|
code = f"{self._prefix}{self.oss_seq:03d}"
|
||||||
|
self.oss_seq += 1
|
||||||
|
else:
|
||||||
|
code = base_prefix
|
||||||
|
if self._oss_enabled:
|
||||||
|
try:
|
||||||
|
compressed = compress_for_oss(pth, str(Path(pth).with_suffix(".oss.jpg")))
|
||||||
|
url = upload_to_oss(self._oss_cfg, compressed,
|
||||||
|
build_oss_key(self.country, self.task_timestamp,
|
||||||
|
code, _gen_rand4()))
|
||||||
|
if url:
|
||||||
|
urls.append(url)
|
||||||
|
r["seed_shot_urls"] = urls
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[pinterest_pipeline] 种草图上传失败 {pth}: {e}")
|
||||||
@@ -47,10 +47,30 @@ def list_colors(db_path, spu_code: str) -> List[Dict[str, Any]]:
|
|||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_name(s: str) -> str:
|
||||||
|
"""去空格(半角+全角)+ 小写,用于 SKU code 与文件夹名比较。"""
|
||||||
|
return str(s or "").replace(" ", "").replace(" ", "").strip().lower()
|
||||||
|
|
||||||
|
|
||||||
def find_basemap(basemap_root, spu_code: str, sku_code: str) -> Optional[Path]:
|
def find_basemap(basemap_root, spu_code: str, sku_code: str) -> Optional[Path]:
|
||||||
"""basemap/<款号>/<SKU.code>/ 下第一张图片;无返回 None。"""
|
"""basemap/<款号>/<SKU.code>/ 下第一张图片;无返回 None。
|
||||||
d = Path(basemap_root) / spu_code / sku_code
|
|
||||||
if not d.exists():
|
SKU code 与文件夹名比较时两边都去空格(兼容 db code 或文件夹名带空格)。
|
||||||
|
"""
|
||||||
|
root = Path(basemap_root) / spu_code
|
||||||
|
if not root.exists():
|
||||||
|
return None
|
||||||
|
target = _norm_name(sku_code)
|
||||||
|
if not target:
|
||||||
|
return None
|
||||||
|
d = root / sku_code
|
||||||
|
if not (d.exists() and d.is_dir()):
|
||||||
|
d = None
|
||||||
|
for cand in sorted(root.iterdir()):
|
||||||
|
if cand.is_dir() and _norm_name(cand.name) == target:
|
||||||
|
d = cand
|
||||||
|
break
|
||||||
|
if d is None:
|
||||||
return None
|
return None
|
||||||
for f in sorted(d.iterdir()):
|
for f in sorted(d.iterdir()):
|
||||||
if f.is_file() and f.suffix.lower() in IMG_EXTS:
|
if f.is_file() and f.suffix.lower() in IMG_EXTS:
|
||||||
|
|||||||
@@ -44,15 +44,55 @@ def load_templates() -> List[Dict[str, str]]:
|
|||||||
for t in tpls if t.get("prompt")]
|
for t in tpls if t.get("prompt")]
|
||||||
|
|
||||||
|
|
||||||
def load_model_features() -> List[str]:
|
def load_model_features(gender: Optional[str] = None) -> List[str]:
|
||||||
"""模特特征列表(无配置时给内置兜底)。"""
|
"""模特特征列表(无配置时给内置兜底)。
|
||||||
|
gender: "male"/"female" 时只返回对应性别;None/其他 返回全部(指定性别组为空时回退全部)。"""
|
||||||
data = _load_yaml("configs/model_features.yaml")
|
data = _load_yaml("configs/model_features.yaml")
|
||||||
feats = [str(f) for f in (data.get("model_features") or []) if str(f).strip()]
|
mf = data.get("model_features") or []
|
||||||
|
feats: List[str] = []
|
||||||
|
if isinstance(mf, dict):
|
||||||
|
if gender and gender in mf:
|
||||||
|
feats = [str(f) for f in mf[gender] if str(f).strip()]
|
||||||
|
if not feats:
|
||||||
|
feats = [str(f) for g in mf.values() for f in g if str(f).strip()]
|
||||||
|
else:
|
||||||
|
feats = [str(f) for f in mf if str(f).strip()]
|
||||||
if not feats:
|
if not feats:
|
||||||
feats = ["20岁清新少女,素颜通透感", "25岁都市职场女性,干练气质"]
|
feats = ["20岁清新少女,素颜通透感", "25岁都市职场女性,干练气质"]
|
||||||
return feats
|
return feats
|
||||||
|
|
||||||
|
|
||||||
|
def read_template_category(template_path: str) -> str:
|
||||||
|
"""读取模版「类目」表头对应的值(如 服装、鞋靴和珠宝饰品>男士时尚>男装>男装上衣、T恤、衬衫>男装T恤)。
|
||||||
|
遍历所有 sheet(类目表头可能在「模版」等 sheet),找到即返回其下一行同列值。"""
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
wb = openpyxl.load_workbook(template_path, data_only=True, read_only=True)
|
||||||
|
try:
|
||||||
|
for ws in wb.worksheets:
|
||||||
|
rows = [r for r in ws.iter_rows(min_row=1, max_row=5, values_only=True)]
|
||||||
|
for ri, row in enumerate(rows):
|
||||||
|
for ci, v in enumerate(row):
|
||||||
|
if v is not None and str(v).strip() == "类目":
|
||||||
|
if ri + 1 < len(rows):
|
||||||
|
val = rows[ri + 1][ci]
|
||||||
|
return str(val or "").strip()
|
||||||
|
finally:
|
||||||
|
wb.close()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[seed_shot] 读取模版类目失败: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def gender_from_category(category: str) -> Optional[str]:
|
||||||
|
"""类目含「男」→ male;含「女」→ female;都不含 → None(全部随机)。"""
|
||||||
|
if "男" in category:
|
||||||
|
return "male"
|
||||||
|
if "女" in category:
|
||||||
|
return "female"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_style_features() -> List[str]:
|
def load_style_features() -> List[str]:
|
||||||
"""服装风格列表(style_features.yaml,随机取一条替换 [服装风格];无配置时内置兜底)。"""
|
"""服装风格列表(style_features.yaml,随机取一条替换 [服装风格];无配置时内置兜底)。"""
|
||||||
data = _load_yaml("configs/style_features.yaml")
|
data = _load_yaml("configs/style_features.yaml")
|
||||||
@@ -75,13 +115,15 @@ def render_prompt(template_prompt: str, cn_title: str, material: str, model_feat
|
|||||||
|
|
||||||
def generate_seed_shots(image_backend, base_image: str, cn_title: str, material: str,
|
def generate_seed_shots(image_backend, base_image: str, cn_title: str, material: str,
|
||||||
count: int, out_dir: str, negative: str = "",
|
count: int, out_dir: str, negative: str = "",
|
||||||
size: str = "1504x2000", prefix: str = "") -> List[str]:
|
size: str = "1536x2048", prefix: str = "",
|
||||||
|
gender: Optional[str] = None) -> List[str]:
|
||||||
"""生成 count 张种草图(img2img,图1=合成图)。返回产物路径列表。
|
"""生成 count 张种草图(img2img,图1=合成图)。返回产物路径列表。
|
||||||
size: 种草图统一 1504x2000。
|
size: 种草图统一 1536x2048(与合成图一致)。
|
||||||
prefix: 货号前缀(对应产品货号,命名 {prefix}_seedshot_{n}.png,不覆盖旧文件)。
|
prefix: 货号前缀(对应产品货号,命名 {prefix}_{随机4位}.png,不覆盖旧文件)。
|
||||||
|
gender: "male"/"female" 时只从对应性别模特特征随机;None 全部随机。
|
||||||
占位符 [商品名称]/[材质]/[模特特征]/[服装风格] 均随机组合(模板/模特/服装风格各随机取一条)。"""
|
占位符 [商品名称]/[材质]/[模特特征]/[服装风格] 均随机组合(模板/模特/服装风格各随机取一条)。"""
|
||||||
templates = load_templates()
|
templates = load_templates()
|
||||||
features = load_model_features()
|
features = load_model_features(gender=gender)
|
||||||
style_features = load_style_features()
|
style_features = load_style_features()
|
||||||
out = Path(out_dir)
|
out = Path(out_dir)
|
||||||
out.mkdir(parents=True, exist_ok=True)
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -91,8 +133,13 @@ def generate_seed_shots(image_backend, base_image: str, cn_title: str, material:
|
|||||||
feat = random.choice(features)
|
feat = random.choice(features)
|
||||||
style_feat = random.choice(style_features)
|
style_feat = random.choice(style_features)
|
||||||
prompt = render_prompt(tpl["prompt"], cn_title, material, feat, style_feat)
|
prompt = render_prompt(tpl["prompt"], cn_title, material, feat, style_feat)
|
||||||
out_path = str(out / f"{prefix}_seedshot_{i + 1:02d}.png" if prefix
|
# 命名:{货号}_{随机4位}.png(按货号命名,随机4位避免自增序号/覆盖)
|
||||||
else out / f"seed_shot_{i + 1:02d}.png")
|
while True:
|
||||||
|
rand4 = f"{random.randint(0, 9999):04d}"
|
||||||
|
out_path = str(out / f"{prefix}_{rand4}.png" if prefix
|
||||||
|
else out / f"seed_{rand4}.png")
|
||||||
|
if not Path(out_path).exists():
|
||||||
|
break
|
||||||
try:
|
try:
|
||||||
image_backend.print(prompt, base_image, out_path, negative, size=size)
|
image_backend.print(prompt, base_image, out_path, negative, size=size)
|
||||||
paths.append(out_path)
|
paths.append(out_path)
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ class AgentState(TypedDict, total=False):
|
|||||||
pinterest_images: Dict[str, List[str]] # pinterest_scrape 产出:搜索词 → 爬取图片路径列表
|
pinterest_images: Dict[str, List[str]] # pinterest_scrape 产出:搜索词 → 爬取图片路径列表
|
||||||
pinterest_briefs: List[Dict[str, Any]] # pinterest_analyze 产出:LLM 分析图片的原始设计简报
|
pinterest_briefs: List[Dict[str, Any]] # pinterest_analyze 产出:LLM 分析图片的原始设计简报
|
||||||
|
|
||||||
|
# —— Pinterest 按需搜索循环状态 ——
|
||||||
|
pinterest_target: int # 目标简报数(= spu_tasks 数量,每款一个设计)
|
||||||
|
pinterest_rounds: int # 已搜索轮次
|
||||||
|
pinterest_attempted: List[str] # 本轮已尝试(未持久化)的搜索词,防同轮重复
|
||||||
|
pinterest_pipeline: Any # PinterestPipeline 实例(简报池 + 并发生成线程)
|
||||||
|
|
||||||
# —— 可观测性 ——
|
# —— 可观测性 ——
|
||||||
errors: List[Dict[str, Any]] # 各节点兜底捕获的错误:{node, type, message, trace}
|
errors: List[Dict[str, Any]] # 各节点兜底捕获的错误:{node, type, message, trace}
|
||||||
stats: Dict[str, Any] # 各阶段统计:{fetch, filter, score, screen, prompt, compose}
|
stats: Dict[str, Any] # 各阶段统计:{fetch, filter, score, screen, prompt, compose}
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ from graph.product import _connect
|
|||||||
# 商品轮播图列名关键词(模板存在中/英/日变体,如 商品轮播图1 / Product Carousel Image 1 / 商品カルーセル画像1)
|
# 商品轮播图列名关键词(模板存在中/英/日变体,如 商品轮播图1 / Product Carousel Image 1 / 商品カルーセル画像1)
|
||||||
_CAROUSEL_KW = ("轮播", "carousel", "カルーセル")
|
_CAROUSEL_KW = ("轮播", "carousel", "カルーセル")
|
||||||
|
|
||||||
|
# 商品产地:国家简称 → 正式名称(模版要求,如「沙特站」提取为「沙特」但需填「沙特阿拉伯」)
|
||||||
|
_COUNTRY_NAME_MAP = {
|
||||||
|
"沙特": "沙特阿拉伯",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _size_rank(size) -> tuple:
|
def _size_rank(size) -> tuple:
|
||||||
"""把尺码字符串转成可排序 rank(从小到大)。
|
"""把尺码字符串转成可排序 rank(从小到大)。
|
||||||
@@ -99,15 +104,29 @@ def _ja_col(router) -> Optional[int]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _es_col(router) -> Optional[int]:
|
||||||
|
"""定位「西语名称」列(基础信息组,如 西语名称/西班牙语名称)。"""
|
||||||
|
try:
|
||||||
|
return router.resolve_col("西语名称")
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
for name, col in router.column_map.items():
|
||||||
|
low = str(name)
|
||||||
|
if ("西语" in low or "西班牙" in low) and "详情图文" not in low and "轮播图" not in low and "名称" in low:
|
||||||
|
return col
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_title: str,
|
def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_title: str,
|
||||||
ja_title: str, composite_by_sku: Dict[str, Any],
|
ja_title: str, es_title: str, composite_by_sku: Dict[str, Any],
|
||||||
all_composite_urls: List[str], seed_shot_urls: List[str],
|
all_composite_urls: List[str], seed_shot_urls: List[str],
|
||||||
only_rows: Optional[List[int]] = None) -> None:
|
only_rows: Optional[List[int]] = None) -> None:
|
||||||
"""按用户要求填充设计联动字段:
|
"""按用户要求填充设计联动字段:
|
||||||
- SPU 行:SPU货号=设计货号、SKU货号=设计货号、商品名称=cn_title、英文名称=en_title、
|
- SPU 行:SPU货号=设计货号、SKU货号=设计货号、商品名称=en_title、英文名称=en_title、
|
||||||
日语名称=ja_title、商品轮播图1=随机一张三合一主图、详情图文=全部主图+种草图链接 | 分割
|
日语名称=ja_title、西语名称=es_title、商品轮播图1=随机一张三合一主图、
|
||||||
|
详情图文=全部主图+种草图 链接 | 分割(不含 img_url_2)
|
||||||
- SKU 行:SPU货号=设计货号、SKU货号=该颜色货号、商品轮播图1=该颜色三合一链接、
|
- SKU 行:SPU货号=设计货号、SKU货号=该颜色货号、商品轮播图1=该颜色三合一链接、
|
||||||
商品名称/英文名称/日语名称 与 SPU 一致
|
商品名称/英文名称/日语名称/西语名称 与 SPU 一致
|
||||||
only_rows:合并模式下只填充本产品块的行(None=该款全部行)
|
only_rows:合并模式下只填充本产品块的行(None=该款全部行)
|
||||||
"""
|
"""
|
||||||
import random
|
import random
|
||||||
@@ -131,6 +150,10 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
|
|||||||
ja_col = _ja_col(router)
|
ja_col = _ja_col(router)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
ja_col = None
|
ja_col = None
|
||||||
|
try:
|
||||||
|
es_col = _es_col(router)
|
||||||
|
except KeyError:
|
||||||
|
es_col = None
|
||||||
try:
|
try:
|
||||||
sku_code_col = router.resolve_col("SKU货号")
|
sku_code_col = router.resolve_col("SKU货号")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -148,12 +171,14 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
|
|||||||
router.ws.cell(row, spu_col, oss_code)
|
router.ws.cell(row, spu_col, oss_code)
|
||||||
if sku_code_col and oss_code:
|
if sku_code_col and oss_code:
|
||||||
router.ws.cell(row, sku_code_col, oss_code)
|
router.ws.cell(row, sku_code_col, oss_code)
|
||||||
if name_col and cn_title:
|
if name_col and en_title:
|
||||||
router.ws.cell(row, name_col, cn_title)
|
router.ws.cell(row, name_col, en_title) # 商品名称统一用 en_title
|
||||||
if en_col and en_title:
|
if en_col and en_title:
|
||||||
router.ws.cell(row, en_col, en_title)
|
router.ws.cell(row, en_col, en_title)
|
||||||
if ja_col and ja_title:
|
if ja_col and ja_title:
|
||||||
router.ws.cell(row, ja_col, ja_title)
|
router.ws.cell(row, ja_col, ja_title)
|
||||||
|
if es_col and es_title:
|
||||||
|
router.ws.cell(row, es_col, es_title)
|
||||||
if car1 is not None and all_composite_urls:
|
if car1 is not None and all_composite_urls:
|
||||||
router.ws.cell(row, car1, random.choice(all_composite_urls)) # SPU 轮播图1 随机
|
router.ws.cell(row, car1, random.choice(all_composite_urls)) # SPU 轮播图1 随机
|
||||||
if detail_col is not None:
|
if detail_col is not None:
|
||||||
@@ -163,13 +188,15 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
|
|||||||
else:
|
else:
|
||||||
if spu_col and oss_code:
|
if spu_col and oss_code:
|
||||||
router.ws.cell(row, spu_col, oss_code)
|
router.ws.cell(row, spu_col, oss_code)
|
||||||
# SKU 行与 SPU 一致:商品名称/英文名称/日语名称
|
# SKU 行与 SPU 一致:商品名称/英文名称/日语名称/西语名称
|
||||||
if name_col and cn_title:
|
if name_col and en_title:
|
||||||
router.ws.cell(row, name_col, cn_title)
|
router.ws.cell(row, name_col, en_title) # 商品名称统一用 en_title
|
||||||
if en_col and en_title:
|
if en_col and en_title:
|
||||||
router.ws.cell(row, en_col, en_title)
|
router.ws.cell(row, en_col, en_title)
|
||||||
if ja_col and ja_title:
|
if ja_col and ja_title:
|
||||||
router.ws.cell(row, ja_col, ja_title)
|
router.ws.cell(row, ja_col, ja_title)
|
||||||
|
if es_col and es_title:
|
||||||
|
router.ws.cell(row, es_col, es_title)
|
||||||
cc = composite_by_sku.get(color) or composite_by_sku.get("") # 按色值匹配该颜色主图
|
cc = composite_by_sku.get(color) or composite_by_sku.get("") # 按色值匹配该颜色主图
|
||||||
if sku_code_col and oss_code:
|
if sku_code_col and oss_code:
|
||||||
router.ws.cell(row, sku_code_col, oss_code) # SKU货号=SPU货号(同一货号)
|
router.ws.cell(row, sku_code_col, oss_code) # SKU货号=SPU货号(同一货号)
|
||||||
@@ -185,6 +212,7 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
|
|||||||
"SKC货号": spu_code, # code 路由为 SKC货号(用户要求)
|
"SKC货号": spu_code, # code 路由为 SKC货号(用户要求)
|
||||||
"风格": "休闲", # style 路由为"休闲"(用户要求)
|
"风格": "休闲", # style 路由为"休闲"(用户要求)
|
||||||
"商品产地": origin_province, # 产地省份不用填,经营站点填到「商品产地」
|
"商品产地": origin_province, # 产地省份不用填,经营站点填到「商品产地」
|
||||||
|
"款式来源": "现货款", # SPU商品属性-款式来源 统一填「现货款」(用户要求)
|
||||||
}
|
}
|
||||||
if color:
|
if color:
|
||||||
row["色值(主规格)"] = color
|
row["色值(主规格)"] = color
|
||||||
@@ -195,19 +223,26 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
|
|||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
def _find_price_header(router) -> str:
|
def _find_price_headers(router) -> List[str]:
|
||||||
"""定位价格列表头:任意含「申报价格」的列(美站/日站/英站…模糊匹配);找不到回退默认。"""
|
"""定位所有「申报价格」列(美站/日站/英站…模糊匹配);匹配到多个时全部返回,统一填加价后价格。"""
|
||||||
for k in router.column_map:
|
hits = [str(k) for k in router.column_map if "申报价格" in str(k)]
|
||||||
if "申报价格" in str(k):
|
return hits or ["申报价格-日本站"]
|
||||||
return str(k)
|
|
||||||
return "申报价格-日本站"
|
|
||||||
|
def _find_bust_headers(router) -> List[str]:
|
||||||
|
"""定位所有「胸围」列(基码表-胸围(cm)/胸围全围(cm)…模糊匹配);匹配到多个时全部填 sku.bust。"""
|
||||||
|
hits = [str(k) for k in router.column_map if "胸围" in str(k)]
|
||||||
|
return hits or ["胸围全围(cm)"]
|
||||||
|
|
||||||
|
|
||||||
def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color: str,
|
def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color: str,
|
||||||
warehouses: List[str], markup_percent: float = 0.0,
|
warehouses: List[str], markup_percent: float = 0.0,
|
||||||
multi: bool = True, price_header: str = "申报价格-日本站") -> Dict[str, Any]:
|
multi: bool = True, price_header: str = "申报价格-日本站",
|
||||||
|
bust_headers: Optional[List[str]] = None,
|
||||||
|
price_headers: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||||
"""构造一行 SKU(固定字段:SPU货号、SKC货号=sku.code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。
|
"""构造一行 SKU(固定字段:SPU货号、SKC货号=sku.code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。
|
||||||
价格(price_header 列,如 申报价格-美国站/日本站,模糊匹配)= SKU.price × (1+markup/100),预先填好。
|
价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100),
|
||||||
|
预先填好。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。
|
||||||
规格类型2 统一填「尺码」两个字(不是 size 参数值)。"""
|
规格类型2 统一填「尺码」两个字(不是 size 参数值)。"""
|
||||||
row: Dict[str, Any] = {
|
row: Dict[str, Any] = {
|
||||||
"基础信息-商品层级": "sku",
|
"基础信息-商品层级": "sku",
|
||||||
@@ -217,17 +252,30 @@ def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color:
|
|||||||
"规格类型2": "尺码", # 规格类型2 统一填「尺码」(不填 size 值)
|
"规格类型2": "尺码", # 规格类型2 统一填「尺码」(不填 size 值)
|
||||||
"币种": "CNY",
|
"币种": "CNY",
|
||||||
}
|
}
|
||||||
|
if price_headers is None:
|
||||||
|
price_headers = [price_header]
|
||||||
|
if bust_headers is None:
|
||||||
|
bust_headers = ["胸围全围(cm)"]
|
||||||
for j, w in enumerate(warehouses, start=1):
|
for j, w in enumerate(warehouses, start=1):
|
||||||
row[f"发货仓{j}"] = w
|
row[f"发货仓{j}"] = w
|
||||||
row[f"发货仓{j}库存"] = 200
|
row[f"发货仓{j}库存"] = 200
|
||||||
for dbk, header in SKU_MAP.items():
|
for dbk, header in SKU_MAP.items():
|
||||||
if dbk == "color":
|
if dbk == "color":
|
||||||
continue
|
continue
|
||||||
if dbk == "price":
|
|
||||||
header = price_header # 模糊匹配的实际价格列(申报价格-美站/日站/英站…)
|
|
||||||
v = sk.get(dbk)
|
v = sk.get(dbk)
|
||||||
if dbk == "price" and v not in (None, ""):
|
if dbk == "bust":
|
||||||
|
if v in (None, ""):
|
||||||
|
continue
|
||||||
|
for h in bust_headers:
|
||||||
|
row[h] = v
|
||||||
|
continue
|
||||||
|
if dbk == "price":
|
||||||
|
if v in (None, ""):
|
||||||
|
continue
|
||||||
v = round(float(v) * (1 + markup_percent / 100), 2) # 申报价格 = price × (1+加价%)
|
v = round(float(v) * (1 + markup_percent / 100), 2) # 申报价格 = price × (1+加价%)
|
||||||
|
for h in price_headers:
|
||||||
|
row[h] = v
|
||||||
|
continue
|
||||||
if v not in (None, ""):
|
if v not in (None, ""):
|
||||||
row[header] = v
|
row[header] = v
|
||||||
return row
|
return row
|
||||||
@@ -325,6 +373,7 @@ def _read_meta(router) -> tuple:
|
|||||||
site = str(ws.cell(2, 1).value or "").strip()
|
site = str(ws.cell(2, 1).value or "").strip()
|
||||||
raw = str(ws.cell(2, 2).value or "").strip()
|
raw = str(ws.cell(2, 2).value or "").strip()
|
||||||
origin_province = site[:-1] if site.endswith("站") else site
|
origin_province = site[:-1] if site.endswith("站") else site
|
||||||
|
origin_province = _COUNTRY_NAME_MAP.get(origin_province, origin_province) # 简称→正式名称(如 沙特→沙特阿拉伯)
|
||||||
warehouses = [w.strip() for w in raw.split("、") if w.strip()]
|
warehouses = [w.strip() for w in raw.split("、") if w.strip()]
|
||||||
return origin_province, warehouses
|
return origin_province, warehouses
|
||||||
|
|
||||||
@@ -342,42 +391,36 @@ def _read_skus(db_path, spu_code: str, sku_code: str) -> List[Dict[str, Any]]:
|
|||||||
return skus
|
return skus
|
||||||
|
|
||||||
|
|
||||||
def export_product(
|
def _import_router(template_dir: str) -> None:
|
||||||
db_path,
|
"""import template_router(优先 config 的 template_dir;打包后回退 _MEIPASS/model;再兜底项目自带 templates/)。"""
|
||||||
spu_code: str,
|
tdir = Path(template_dir)
|
||||||
sku_code, # str | List[str]:单颜色或多个颜色
|
candidates = [tdir]
|
||||||
template_dir: str,
|
meipass = getattr(sys, "_MEIPASS", None)
|
||||||
template_path: str,
|
if meipass:
|
||||||
out_path: str,
|
candidates.append(Path(meipass) / "model")
|
||||||
images: Optional[List[str]] = None,
|
# 用户上传的模板可能在任意目录(无 template_router.py),兜底项目自带 templates/
|
||||||
|
from graph.paths import project_root as _proj_root
|
||||||
|
candidates.append(_proj_root() / "templates")
|
||||||
|
for d in candidates:
|
||||||
|
if d.exists() and str(d) not in sys.path:
|
||||||
|
sys.path.insert(0, str(d))
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_product_block(
|
||||||
|
router, db_path, spu_code, sku_code,
|
||||||
|
origin_province, warehouses, price_headers,
|
||||||
|
markup_percent: float = 0.0, images: Optional[List[str]] = None,
|
||||||
spu_per_color: bool = True,
|
spu_per_color: bool = True,
|
||||||
oss_code: str = "",
|
oss_code: str = "", cn_title: str = "", en_title: str = "", ja_title: str = "",
|
||||||
cn_title: str = "",
|
es_title: str = "",
|
||||||
en_title: str = "",
|
|
||||||
ja_title: str = "",
|
|
||||||
composite_urls: Optional[List[Dict[str, Any]]] = None,
|
composite_urls: Optional[List[Dict[str, Any]]] = None,
|
||||||
seed_shot_urls: Optional[List[str]] = None,
|
seed_shot_urls: Optional[List[str]] = None,
|
||||||
append_to: str = "",
|
bust_headers: Optional[List[str]] = None,
|
||||||
markup_percent: float = 0.0,
|
) -> List[int]:
|
||||||
) -> Path:
|
"""在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。
|
||||||
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
|
|
||||||
|
|
||||||
sku_code :SKU 颜色编码,支持单个 str 或多个(list/tuple/逗号分隔字符串)。
|
供单产品 export_product 与批量 export_products 复用(批量时只打开/保存一次)。
|
||||||
spu_per_color :True(默认)= 每个颜色导出一个 SPU 块;False = 单 SPU 下挂所有颜色 SKU 变体。
|
|
||||||
template_dir :template_router.py 所在目录(用于 import)
|
|
||||||
template_path :商品上传模版 xlsx 路径
|
|
||||||
images :生成的产品图路径列表(仅作用于第一个颜色块:SPU 行轮播图1 + SKU 行回退)
|
|
||||||
oss_code :设计货号(前缀+3位计数),SPU货号/SKU货号 列均填它
|
|
||||||
cn_title :商品名称(中文标题)
|
|
||||||
en_title :英文名称(英文标题)
|
|
||||||
ja_title :日语名称(日语标题,JP 模板生成)
|
|
||||||
composite_urls:[{"sku_code","color","url","code"}] 每色三合一主图(含图床链接与货号)
|
|
||||||
seed_shot_urls :种草图图床链接列表(详情图文 | 拼接用)
|
|
||||||
append_to :已有输出文件路径;提供则在其基础上追加本产品块(一次任务多产品合并一个模板)
|
|
||||||
markup_percent :加价百分比,申报价格 = SKU.price × (1+markup/100) 预填
|
|
||||||
返回输出文件路径。
|
|
||||||
"""
|
"""
|
||||||
# 1) 读 db(支持单/多颜色)
|
|
||||||
spu = _read_spu(db_path, spu_code)
|
spu = _read_spu(db_path, spu_code)
|
||||||
if spu is None:
|
if spu is None:
|
||||||
raise ValueError(f"SPU {spu_code} 不存在于 db")
|
raise ValueError(f"SPU {spu_code} 不存在于 db")
|
||||||
@@ -395,31 +438,11 @@ def export_product(
|
|||||||
skus_by_color.append((sc, skus))
|
skus_by_color.append((sc, skus))
|
||||||
images = [str(i) for i in (images or []) if i]
|
images = [str(i) for i in (images or []) if i]
|
||||||
|
|
||||||
# 2) import template_router(优先 config 的 template_dir;打包后回退 _MEIPASS/model)
|
|
||||||
tdir = Path(template_dir)
|
|
||||||
candidates = [tdir]
|
|
||||||
meipass = getattr(sys, "_MEIPASS", None)
|
|
||||||
if meipass:
|
|
||||||
candidates.append(Path(meipass) / "model")
|
|
||||||
# 用户上传的模板可能在任意目录(无 template_router.py),兜底项目自带 templates/
|
|
||||||
from graph.paths import project_root as _proj_root
|
|
||||||
candidates.append(_proj_root() / "templates")
|
|
||||||
for d in candidates:
|
|
||||||
if d.exists() and str(d) not in sys.path:
|
|
||||||
sys.path.insert(0, str(d))
|
|
||||||
from template_router import TemplateRouter # noqa: E402
|
|
||||||
|
|
||||||
# append_to:合并模式从已有输出文件继续追加(一次任务多产品填一个模板)
|
|
||||||
router = TemplateRouter(append_to if append_to else template_path)
|
|
||||||
try:
|
|
||||||
origin_province, warehouses = _read_meta(router)
|
|
||||||
price_header = _find_price_header(router) # 申报价格列(美站/日站/英站…模糊匹配)
|
|
||||||
multi = len(skus_by_color) > 1
|
|
||||||
color_col = router.resolve_col("色值(主规格)")
|
color_col = router.resolve_col("色值(主规格)")
|
||||||
block_rows: List[int] = [] # 本产品块插入的所有行号(_fill_design_fields 只填这些行)
|
block_rows: List[int] = []
|
||||||
|
|
||||||
if spu_per_color:
|
if spu_per_color:
|
||||||
# 3) 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
|
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
|
||||||
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
|
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
|
||||||
block_rows.append(router.insert(
|
block_rows.append(router.insert(
|
||||||
_build_spu_row(spu, spu_code, origin_province),
|
_build_spu_row(spu, spu_code, origin_province),
|
||||||
@@ -435,11 +458,11 @@ def export_product(
|
|||||||
block_rows.append(router.insert(
|
block_rows.append(router.insert(
|
||||||
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
|
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
|
||||||
markup_percent=markup_percent, multi=True,
|
markup_percent=markup_percent, multi=True,
|
||||||
price_header=price_header),
|
bust_headers=bust_headers, price_headers=price_headers),
|
||||||
spu_code=spu_code, match="exact",
|
spu_code=spu_code, match="exact",
|
||||||
))
|
))
|
||||||
|
|
||||||
# 3.3) 轮播图:首色 SKU 行轮播图1 = 生成首图;SKU 行按色值填 db url/生成图
|
# 轮播图:首色 SKU 行轮播图1 = 生成首图;SKU 行按色值填 db url/生成图
|
||||||
if ci == 0 and images:
|
if ci == 0 and images:
|
||||||
col1 = _carousel_col(router, 1)
|
col1 = _carousel_col(router, 1)
|
||||||
if col1 is not None:
|
if col1 is not None:
|
||||||
@@ -449,7 +472,7 @@ def export_product(
|
|||||||
sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else []
|
sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else []
|
||||||
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
|
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
|
||||||
else:
|
else:
|
||||||
# 4) 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
|
# 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
|
||||||
block_rows.append(router.insert(
|
block_rows.append(router.insert(
|
||||||
_build_spu_row(spu, spu_code, origin_province), match="exact"))
|
_build_spu_row(spu, spu_code, origin_province), match="exact"))
|
||||||
multi_variant = len(skus_by_color) > 1
|
multi_variant = len(skus_by_color) > 1
|
||||||
@@ -461,14 +484,13 @@ def export_product(
|
|||||||
block_rows.append(router.insert(
|
block_rows.append(router.insert(
|
||||||
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
|
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
|
||||||
markup_percent=markup_percent, multi=multi_variant,
|
markup_percent=markup_percent, multi=multi_variant,
|
||||||
price_header=price_header),
|
bust_headers=bust_headers, price_headers=price_headers),
|
||||||
))
|
))
|
||||||
sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else []
|
sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else []
|
||||||
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
|
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
|
||||||
# 无 SPU 行:首图(轮播图1)由 _fill_design_fields 按 SKU 行填充
|
|
||||||
|
|
||||||
# 5) 设计联动字段:货号/标题/轮播图路由/详情图文(图床链接,| 分割)
|
# 设计联动字段:货号/标题/轮播图路由/详情图文(图床链接,| 分割)
|
||||||
if oss_code or cn_title or en_title or ja_title or composite_urls:
|
if oss_code or cn_title or en_title or ja_title or es_title or composite_urls:
|
||||||
by_sku: Dict[str, Any] = {}
|
by_sku: Dict[str, Any] = {}
|
||||||
all_urls: List[str] = []
|
all_urls: List[str] = []
|
||||||
for cc in (composite_urls or []):
|
for cc in (composite_urls or []):
|
||||||
@@ -477,8 +499,119 @@ def export_product(
|
|||||||
if cc.get("url"):
|
if cc.get("url"):
|
||||||
all_urls.append(str(cc["url"]))
|
all_urls.append(str(cc["url"]))
|
||||||
_fill_design_fields(router, spu_code, oss_code, cn_title, en_title, ja_title,
|
_fill_design_fields(router, spu_code, oss_code, cn_title, en_title, ja_title,
|
||||||
by_sku, all_urls, seed_shot_urls or [], only_rows=block_rows)
|
es_title, by_sku, all_urls, seed_shot_urls or [],
|
||||||
|
only_rows=block_rows)
|
||||||
|
|
||||||
|
return block_rows
|
||||||
|
|
||||||
|
|
||||||
|
def export_product(
|
||||||
|
db_path,
|
||||||
|
spu_code: str,
|
||||||
|
sku_code, # str | List[str]:单颜色或多个颜色
|
||||||
|
template_dir: str,
|
||||||
|
template_path: str,
|
||||||
|
out_path: str,
|
||||||
|
images: Optional[List[str]] = None,
|
||||||
|
spu_per_color: bool = True,
|
||||||
|
oss_code: str = "",
|
||||||
|
cn_title: str = "",
|
||||||
|
en_title: str = "",
|
||||||
|
ja_title: str = "",
|
||||||
|
es_title: str = "",
|
||||||
|
composite_urls: Optional[List[Dict[str, Any]]] = None,
|
||||||
|
seed_shot_urls: Optional[List[str]] = None,
|
||||||
|
append_to: str = "",
|
||||||
|
markup_percent: float = 0.0,
|
||||||
|
) -> Path:
|
||||||
|
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
|
||||||
|
|
||||||
|
sku_code :SKU 颜色编码,支持单个 str 或多个(list/tuple/逗号分隔字符串)。
|
||||||
|
spu_per_color :True(默认)= 每个颜色导出一个 SPU 块;False = 单 SPU 下挂所有颜色 SKU 变体。
|
||||||
|
template_dir :template_router.py 所在目录(用于 import)
|
||||||
|
template_path :商品上传模版 xlsx 路径
|
||||||
|
images :生成的产品图路径列表(仅作用于第一个颜色块:SPU 行轮播图1 + SKU 行回退)
|
||||||
|
oss_code :设计货号(前缀+3位计数),SPU货号/SKU货号 列均填它
|
||||||
|
cn_title :商品名称(中文标题)
|
||||||
|
en_title :英文名称(英文标题)
|
||||||
|
ja_title :日语名称(日语标题,JP 模板生成)
|
||||||
|
es_title :西语名称(西班牙语标题,ES 模板生成)
|
||||||
|
composite_urls:[{"sku_code","color","url","code"}] 每色三合一主图(含图床链接与货号)
|
||||||
|
seed_shot_urls :种草图图床链接列表(详情图文 | 拼接用)
|
||||||
|
append_to :已有输出文件路径;提供则在其基础上追加本产品块(一次任务多产品合并一个模板)
|
||||||
|
markup_percent :加价百分比,申报价格 = SKU.price × (1+markup/100) 预填
|
||||||
|
返回输出文件路径。
|
||||||
|
"""
|
||||||
|
_import_router(template_dir)
|
||||||
|
from template_router import TemplateRouter # noqa: E402
|
||||||
|
|
||||||
|
# append_to:合并模式从已有输出文件继续追加(一次任务多产品填一个模板)
|
||||||
|
router = TemplateRouter(append_to if append_to else template_path)
|
||||||
|
try:
|
||||||
|
origin_province, warehouses = _read_meta(router)
|
||||||
|
price_headers = _find_price_headers(router) # 申报价格列(美站/日站/英站…模糊匹配,多个全填)
|
||||||
|
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
|
||||||
|
_insert_product_block(router, db_path, spu_code, sku_code,
|
||||||
|
origin_province, warehouses, price_headers,
|
||||||
|
markup_percent=markup_percent, images=images,
|
||||||
|
spu_per_color=spu_per_color,
|
||||||
|
oss_code=oss_code, cn_title=cn_title, en_title=en_title,
|
||||||
|
ja_title=ja_title, es_title=es_title,
|
||||||
|
composite_urls=composite_urls,
|
||||||
|
seed_shot_urls=seed_shot_urls,
|
||||||
|
bust_headers=bust_headers)
|
||||||
|
out = router.save(out_path)
|
||||||
|
return Path(out)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
router.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def export_products(
|
||||||
|
db_path,
|
||||||
|
products: List[Dict[str, Any]],
|
||||||
|
template_dir: str,
|
||||||
|
template_path: str,
|
||||||
|
out_path: str,
|
||||||
|
markup_percent: float = 0.0,
|
||||||
|
) -> Path:
|
||||||
|
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
|
||||||
|
|
||||||
|
products 每项字段:spu_code / sku_codes(str 或 list)/ oss_code / cn_title / en_title /
|
||||||
|
ja_title / es_title / composite_urls / seed_shot_urls / images / spu_per_color。
|
||||||
|
相比逐产品调用 export_product(每次全量读写工作簿),批量模式显著提速。
|
||||||
|
"""
|
||||||
|
_import_router(template_dir)
|
||||||
|
from template_router import TemplateRouter # noqa: E402
|
||||||
|
|
||||||
|
router = TemplateRouter(template_path)
|
||||||
|
try:
|
||||||
|
origin_province, warehouses = _read_meta(router)
|
||||||
|
price_headers = _find_price_headers(router) # 申报价格列(美站/日站/英站…模糊匹配,多个全填)
|
||||||
|
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
|
||||||
|
for r in products:
|
||||||
|
try:
|
||||||
|
_insert_product_block(
|
||||||
|
router, db_path, r.get("spu_code", ""),
|
||||||
|
r.get("sku_codes") or r.get("sku_code") or "",
|
||||||
|
origin_province, warehouses, price_headers,
|
||||||
|
markup_percent=markup_percent,
|
||||||
|
images=r.get("images"),
|
||||||
|
spu_per_color=bool(r.get("spu_per_color", True)),
|
||||||
|
oss_code=r.get("oss_code", ""),
|
||||||
|
cn_title=r.get("cn_title", ""),
|
||||||
|
en_title=r.get("en_title", ""),
|
||||||
|
ja_title=r.get("ja_title", ""),
|
||||||
|
es_title=r.get("es_title", ""),
|
||||||
|
composite_urls=r.get("composite_urls"),
|
||||||
|
seed_shot_urls=r.get("seed_shot_urls"),
|
||||||
|
bust_headers=bust_headers,
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}")
|
||||||
|
continue
|
||||||
out = router.save(out_path)
|
out = router.save(out_path)
|
||||||
return Path(out)
|
return Path(out)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -10,18 +10,17 @@ v3:模板按国家区分(COUNTRY_TEMPLATES),每国有自己的设计风
|
|||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
# —— 通用固定段(所有国家共用,保证结构一致)——
|
# —— 通用固定段(所有国家共用,保证结构一致)——
|
||||||
# 尺寸规则:最小约 15×18cm ~ 最大 26×32cm 之间自由选择(防模型默认出满幅大图)
|
# 尺寸规则:印花必须「小且居中、四周大量留白」,禁止满幅/自带背景(防模型默认出满幅大图)
|
||||||
SIZE_RULE = (
|
SIZE_RULE = (
|
||||||
"size: choose freely between a MINIMUM print area of about 15x18 cm "
|
"size: the print artwork must be SMALL and CENTERED with clearly larger white margins "
|
||||||
"and a MAXIMUM of 26x32 cm, any size in this range fits, "
|
"around it, print area between about 15x18 cm and 26x32 cm, keep proportions, "
|
||||||
"pick the one that best suits the design, keep proportions, "
|
"scale naturally to the content, do NOT stretch, do NOT fill the entire canvas, "
|
||||||
"scale naturally to the content, do NOT stretch, "
|
"do NOT force full-bleed, leave wide balanced white margins around the artwork"
|
||||||
"do NOT fill the entire canvas, do NOT force full-bleed, "
|
|
||||||
"leave balanced margins around the artwork"
|
|
||||||
)
|
)
|
||||||
# 排除段:无衣服/模特/场景/水印
|
# 排除段:纯白底/无背景/无衣服/模特/场景/水印
|
||||||
NEG_FIXED = (
|
NEG_FIXED = (
|
||||||
"no garment, no shirt, no model, no mannequin, no background scene, no watermark"
|
"isolated on pure white background, no background, no scene, no texture, no frame, no border, "
|
||||||
|
"no garment, no shirt, no model, no mannequin, no watermark"
|
||||||
)
|
)
|
||||||
# 文字规则(v3):可加可不加、适配印花即可;任何文字严禁敏感内容
|
# 文字规则(v3):可加可不加、适配印花即可;任何文字严禁敏感内容
|
||||||
# 注意:正向提示词不写敏感词(no politics/no hate/no violence/no sexual…会被图像审核误判),
|
# 注意:正向提示词不写敏感词(no politics/no hate/no violence/no sexual…会被图像审核误判),
|
||||||
@@ -71,6 +70,7 @@ DEFAULT_TEMPLATES: Dict[str, str] = {
|
|||||||
"studio lighting, e-commerce product photo, no human model"
|
"studio lighting, e-commerce product photo, no human model"
|
||||||
),
|
),
|
||||||
# 复合提示词(三图模特合成):图1=模特 / 图2=纯印花设计稿 / 图3=平铺底图 → 模特穿着成品
|
# 复合提示词(三图模特合成):图1=模特 / 图2=纯印花设计稿 / 图3=平铺底图 → 模特穿着成品
|
||||||
|
# 设计已由图2提供,不再附加 DESIGN CONTENT 四要素描述
|
||||||
"composite_prompt": (
|
"composite_prompt": (
|
||||||
"【图片角色,按提交顺序】图1=模特实拍图(基底);图2=纯印花设计稿;"
|
"【图片角色,按提交顺序】图1=模特实拍图(基底);图2=纯印花设计稿;"
|
||||||
"图3=平铺衣服底图(颜色/面料来源)。\n"
|
"图3=平铺衣服底图(颜色/面料来源)。\n"
|
||||||
@@ -85,8 +85,7 @@ DEFAULT_TEMPLATES: Dict[str, str] = {
|
|||||||
"杜绝“贴纸感”与“平面涂色感”。\n"
|
"杜绝“贴纸感”与“平面涂色感”。\n"
|
||||||
"5.光影融合:按图1环境光方向调整亮度/对比度,印花受光影响产生明暗变化但色号不偏移。\n"
|
"5.光影融合:按图1环境光方向调整亮度/对比度,印花受光影响产生明暗变化但色号不偏移。\n"
|
||||||
"6.纯净输出:仅输出一张最终合成图;图1背景/人物/构图/光影100%不变,"
|
"6.纯净输出:仅输出一张最终合成图;图1背景/人物/构图/光影100%不变,"
|
||||||
"仅替换衣服印花与底色。\n"
|
"仅替换衣服印花与底色。"
|
||||||
"DESIGN CONTENT: {motif}, {art_style}, {color_palette}, {composition}."
|
|
||||||
),
|
),
|
||||||
# 复合负向(印图专用)
|
# 复合负向(印图专用)
|
||||||
"composite_negative": (
|
"composite_negative": (
|
||||||
|
|||||||
|
After Width: | Height: | Size: 415 KiB |
@@ -233,6 +233,14 @@ async def download_image(session: aiohttp.ClientSession, url: str, idx: int,
|
|||||||
else:
|
else:
|
||||||
print(f"❌ 下载失败 {url} 状态码: {resp.status}")
|
print(f"❌ 下载失败 {url} 状态码: {resp.status}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
msg = str(e)
|
||||||
|
# 连接类错误(直连被墙/代理失效)给出明确诊断,避免用户误以为代码问题
|
||||||
|
if any(k in msg.lower() for k in ("cannot connect", "connection refused",
|
||||||
|
"timed out", "timeout", "getaddrinfo",
|
||||||
|
"connection reset", "ssl")):
|
||||||
|
print(f"⚠️ 下载错误 {url}: {e}\n"
|
||||||
|
f" → 无法连接 i.pinimg.com,请确认代理/VPN 已开启且能访问 Pinterest(当前代理: {proxy or '无,直连'})")
|
||||||
|
else:
|
||||||
print(f"⚠️ 下载错误 {url}: {e}")
|
print(f"⚠️ 下载错误 {url}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ SPU_CODE_COL = 2 # B 列:SPU货号
|
|||||||
VALID_LEVELS = ("spu", "sku", "单sku商品")
|
VALID_LEVELS = ("spu", "sku", "单sku商品")
|
||||||
|
|
||||||
_GROUP_BRACKET = re.compile(r"([^)]*)|\([^)]*\)") # 去掉分组名里的括号说明
|
_GROUP_BRACKET = re.compile(r"([^)]*)|\([^)]*\)") # 去掉分组名里的括号说明
|
||||||
|
_UNIT_SUFFIX = re.compile(r"[((](?:cm|g|kg|mm|m²|m2)[))]\s*$", re.IGNORECASE) # 列名尾部单位(肩宽(cm)→肩宽)
|
||||||
|
|
||||||
|
|
||||||
class TemplateRouter:
|
class TemplateRouter:
|
||||||
@@ -333,7 +334,11 @@ class TemplateRouter:
|
|||||||
# 数据插入
|
# 数据插入
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def resolve_col(self, key: str | int) -> int:
|
def resolve_col(self, key: str | int) -> int:
|
||||||
"""把键解析为列号:完整表头 / 列名 / 列号 / Excel 列字母。"""
|
"""把键解析为列号:完整表头 / 列名 / 列号 / Excel 列字母。
|
||||||
|
|
||||||
|
精确匹配失败时做单位归一化匹配(去掉尾部(cm)/(g)…),
|
||||||
|
兼容「肩宽(cm)」↔「肩宽」这类带单位/不带单位的列名差异。
|
||||||
|
"""
|
||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
return key
|
return key
|
||||||
key_s = str(key).strip()
|
key_s = str(key).strip()
|
||||||
@@ -343,6 +348,15 @@ class TemplateRouter:
|
|||||||
return self.column_map[key_s]
|
return self.column_map[key_s]
|
||||||
if re.fullmatch(r"[A-Za-z]{1,3}", key_s):
|
if re.fullmatch(r"[A-Za-z]{1,3}", key_s):
|
||||||
return column_index_from_string(key_s.upper())
|
return column_index_from_string(key_s.upper())
|
||||||
|
norm = _UNIT_SUFFIX.sub("", key_s).strip()
|
||||||
|
if norm and norm != key_s:
|
||||||
|
if norm in self.column_map:
|
||||||
|
return self.column_map[norm]
|
||||||
|
if norm in self.header_map:
|
||||||
|
return self.header_map[norm]
|
||||||
|
for name, col in self.column_map.items():
|
||||||
|
if _UNIT_SUFFIX.sub("", str(name)).strip() == norm:
|
||||||
|
return col
|
||||||
raise KeyError(f"无法识别的表头: {key!r}(可用完整表头/列名/列号/列字母)")
|
raise KeyError(f"无法识别的表头: {key!r}(可用完整表头/列名/列号/列字母)")
|
||||||
|
|
||||||
def _value_by_col(self, data: dict, name: str):
|
def _value_by_col(self, data: dict, name: str):
|
||||||
|
|||||||
@@ -546,12 +546,6 @@ class App(tk.Tk):
|
|||||||
ttk.Label(top, textvariable=self.hotspot_time_var, anchor="e",
|
ttk.Label(top, textvariable=self.hotspot_time_var, anchor="e",
|
||||||
foreground="#185FA5").pack(side="right")
|
foreground="#185FA5").pack(side="right")
|
||||||
|
|
||||||
# —— OpenAI 配置说明(所有配置从 config.yaml 读取:key/URL/模型,前端不配置)——
|
|
||||||
oai = ttk.LabelFrame(self, text="OpenAI 配置(全部读取 config.yaml:API Key / Base URL / 模型,不在前端配置)", padding=4)
|
|
||||||
oai.pack(fill="x", padx=8, pady=(2, 0))
|
|
||||||
ttk.Label(oai, text="LLM / 图像网关与模型在 config.yaml 的 llm_screen / compose 段配置",
|
|
||||||
foreground="#5F5E5A").pack(side="left")
|
|
||||||
|
|
||||||
# —— 商品上传模板(必须上传后才能开始任务;template_export 从该模板解析并填充)——
|
# —— 商品上传模板(必须上传后才能开始任务;template_export 从该模板解析并填充)——
|
||||||
tpl = ttk.LabelFrame(self, text="商品上传模板(必须上传 .xlsx 后才能运行)", padding=4)
|
tpl = ttk.LabelFrame(self, text="商品上传模板(必须上传 .xlsx 后才能运行)", padding=4)
|
||||||
tpl.pack(fill="x", padx=8, pady=(2, 0))
|
tpl.pack(fill="x", padx=8, pady=(2, 0))
|
||||||
@@ -948,6 +942,10 @@ class App(tk.Tk):
|
|||||||
break
|
break
|
||||||
if bm_root is None:
|
if bm_root is None:
|
||||||
bm_root = config_root() / bm_rel
|
bm_root = config_root() / bm_rel
|
||||||
|
|
||||||
|
def _norm(s: str) -> str:
|
||||||
|
return str(s or "").replace(" ", "").replace(" ", "").strip().lower()
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
spu = str(t.get("spu", ""))
|
spu = str(t.get("spu", ""))
|
||||||
@@ -955,8 +953,19 @@ class App(tk.Tk):
|
|||||||
sc = sc.strip()
|
sc = sc.strip()
|
||||||
if not sc:
|
if not sc:
|
||||||
continue
|
continue
|
||||||
d = bm_root / spu / sc
|
# SKU code 与文件夹名两边都去空格后再比较(兼容 db code 或文件夹名带空格)
|
||||||
if not d.is_dir() or not any(p.is_file() for p in d.iterdir()):
|
spu_dir = bm_root / spu
|
||||||
|
if not spu_dir.is_dir():
|
||||||
|
missing.append(f"{spu}/{sc}")
|
||||||
|
continue
|
||||||
|
target = _norm(sc)
|
||||||
|
found = False
|
||||||
|
for cand in spu_dir.iterdir():
|
||||||
|
if (cand.is_dir() and _norm(cand.name) == target
|
||||||
|
and any(p.is_file() for p in cand.iterdir())):
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
missing.append(f"{spu}/{sc}")
|
missing.append(f"{spu}/{sc}")
|
||||||
return missing
|
return missing
|
||||||
|
|
||||||
|
|||||||