模板导出增强 + 模特性别分组 + 三合一提示词精简
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)
This commit is contained in:
@@ -156,16 +156,18 @@ class MockBackend:
|
||||
random.shuffle(pool)
|
||||
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
|
||||
while len(terms) < count and pool:
|
||||
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:
|
||||
terms.append(combo)
|
||||
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}
|
||||
|
||||
def analyze_pinterest_images(self, image_paths, term="", country=""):
|
||||
def analyze_pinterest_images(self, image_paths, term="", country="", on_400=None):
|
||||
"""规则生成设计简报(零 API 成本):按搜索词启发式推导风格/配色/构图。"""
|
||||
from ..classify import classify, prompt_suggestion
|
||||
cat = classify(term)
|
||||
@@ -184,6 +186,8 @@ class MockBackend:
|
||||
"color_palette": palette,
|
||||
"composition": composition,
|
||||
"negative_prompt": negative,
|
||||
"image_prompt": (f"{motif}, {art_style}, {palette}, {composition}, "
|
||||
f"original {art_style} t-shirt print design"),
|
||||
# 生图参考:每条简报对应其来源爬取图(mock 按图逐张产出简报,顺序一一对应)
|
||||
"ref_images": [str(paths[i])] if i < len(paths) else [],
|
||||
"source": "pinterest",
|
||||
|
||||
@@ -15,9 +15,7 @@ from typing import Any, Dict, List
|
||||
import requests
|
||||
|
||||
# 模型调用一律直连:用户常开 VPN(系统代理),LLM 网关多为国内/自建,走代理会被拦截或变慢。
|
||||
# 环境变量级 NO_PROXY 双保险(requests/urllib3 均读取),Google 采集(pytrends)不受影响。
|
||||
os.environ.setdefault("NO_PROXY", "*")
|
||||
os.environ.setdefault("no_proxy", "*")
|
||||
# 仅请求级 proxies=NO_PROXY 直连,不设置进程级 NO_PROXY 环境变量(避免影响 Google Trends 等外部采集)。
|
||||
from .base import LLMBackend
|
||||
from graph.paths import runtime_root
|
||||
|
||||
@@ -107,6 +105,7 @@ Rules:
|
||||
# 模板字典按编号存放;TITLE_TEMPLATE_ROUTE 按国家路由到模板编号。
|
||||
# 模板 1:英语市场(US/GB/AU/MX)→ en_title + cn_title
|
||||
# 模板 2:日本市场(JP)→ en_title + cn_title + ja_title
|
||||
# 模板 3:西班牙市场(ES)→ es_title + cn_title
|
||||
TITLE_TEMPLATES: Dict[str, str] = {
|
||||
"1": '''# Role
|
||||
你是一位资深的跨境服装运营专家,精通英语电商的SEO标题逻辑。你的任务是通过分析服装图片,生成高权重的英语-中文商品标题。
|
||||
@@ -151,15 +150,36 @@ TITLE_TEMPLATES: Dict[str, str] = {
|
||||
|
||||
- **Output**: 必须严格返回 JSON 格式,不要包含 Markdown 代码块标记,格式如下:
|
||||
{"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] = {
|
||||
"US": "1",
|
||||
"GB": "1",
|
||||
"JP": "2",
|
||||
"AU": "1",
|
||||
"MX": "1",
|
||||
"ES": "3",
|
||||
}
|
||||
|
||||
|
||||
@@ -234,11 +254,19 @@ def build_user_prompt(country, topics, aesthetic_hint):
|
||||
|
||||
|
||||
# —— Pinterest 参考模式:搜索词生成(json_schema 结构化 + 动态注入已用词防重复)——
|
||||
PINTEREST_TERM_SYSTEM_PROMPT = """You are a Pinterest search-term generator for print-on-demand (POD) T-shirt design.
|
||||
You turn seed words into diverse, visual, Pinterest-friendly search terms that will be used to scrape inspiration images.
|
||||
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
|
||||
that are DIRECTLY usable as reference for a t-shirt print design.
|
||||
|
||||
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 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.
|
||||
@@ -257,7 +285,7 @@ PINTEREST_TERM_SCHEMA = {
|
||||
"search_terms": {
|
||||
"type": "array",
|
||||
"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"],
|
||||
@@ -267,10 +295,10 @@ PINTEREST_TERM_SCHEMA = {
|
||||
|
||||
|
||||
def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
|
||||
"""动态注入:种子词(灵感)+ 已用搜索词(禁止重复)+ 数量要求。"""
|
||||
"""动态注入:种子词(灵感)+ 已用搜索词(禁止重复)+ 数量要求(按需每次 1 个)。"""
|
||||
seeds = context.get("seeds", []) or []
|
||||
used = context.get("used_terms", []) or []
|
||||
count = int(context.get("count", 10))
|
||||
count = int(context.get("count", 1))
|
||||
lines = [
|
||||
f"Country: {context.get('country', '')}",
|
||||
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"{', '.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)
|
||||
|
||||
|
||||
# —— Pinterest 参考模式:图片分析 → 原创设计简报(多模态)——
|
||||
# image_prompt 由 LLM 直接输出完整的英文生图提示词(多模态对图片的描述拼接),
|
||||
# 不再走「四要素 + 固定模板」装配;尺寸/白底等统一约束段由 prompt_node 自动追加。
|
||||
PINTEREST_ANALYZE_SYSTEM_PROMPT = """You are a POD (print-on-demand) T-shirt design analyst.
|
||||
You receive Pinterest reference images for one search term. For each image, extract the VISUAL CONCEPT
|
||||
(style, mood, motif, color palette, composition) that makes it appealing, then produce an ORIGINAL
|
||||
@@ -301,9 +335,14 @@ RULES:
|
||||
- composition: English layout (e.g. "centered emblem with balanced negative space").
|
||||
- concept: Chinese, one sentence describing the design idea.
|
||||
- negative_prompt: what to avoid (real people, likeness, characters, logos, text).
|
||||
- image_prompt: a COMPLETE, fluent English text-to-image prompt for generating the ORIGINAL flat print
|
||||
design artwork (the print itself, NOT a garment photo). Describe the motif, art style, colors, layout
|
||||
and mood in natural English, as a standalone print. Do NOT include garment / shirt / model / mannequin /
|
||||
background-scene / watermark words. Do NOT mention any size or white-background suffix — a fixed
|
||||
"small centered print on pure white" suffix will be appended automatically by the system.
|
||||
|
||||
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 = {
|
||||
"name": "pinterest_design_briefs",
|
||||
@@ -315,15 +354,17 @@ PINTEREST_ANALYZE_SCHEMA = {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image_index": {"type": "integer"},
|
||||
"motif": {"type": "string"},
|
||||
"art_style": {"type": "string"},
|
||||
"color_palette": {"type": "string"},
|
||||
"composition": {"type": "string"},
|
||||
"concept": {"type": "string"},
|
||||
"negative_prompt": {"type": "string"},
|
||||
"image_prompt": {"type": "string"},
|
||||
},
|
||||
"required": ["motif", "art_style", "color_palette", "composition",
|
||||
"concept", "negative_prompt"],
|
||||
"required": ["image_index", "motif", "art_style", "color_palette",
|
||||
"composition", "concept", "negative_prompt", "image_prompt"],
|
||||
"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"Analyze the attached images and produce {image_count} ORIGINAL design briefs "
|
||||
f"(one per image), each capturing the visual vibe as an original T-shirt print design. "
|
||||
f"Do NOT copy the images."
|
||||
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:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
m = re.search(r"\{.*\}", text, re.S)
|
||||
if m:
|
||||
return json.loads(m.group(0))
|
||||
# 找第一个 { 到与之平衡的 },逐字符跳过字符串内的花括号,避免贪婪匹配截断 JSON
|
||||
start = text.find("{")
|
||||
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
|
||||
|
||||
|
||||
@@ -512,13 +579,17 @@ class OpenAICompatBackend(LLMBackend):
|
||||
raw = _retry(lambda: call_openai_compatible_structured(cfg, messages, PINTEREST_TERM_SCHEMA, timeout=120))
|
||||
parsed = _extract_json(raw)
|
||||
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}
|
||||
|
||||
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 图片 → 原创设计简报列表。
|
||||
|
||||
图片输入不被模型支持(纯文本模型 400)时自动降级为纯文本分析(仅用搜索词)。
|
||||
失败返回 [],由节点兜底(回退 mock 规则简报)。
|
||||
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
|
||||
"""
|
||||
cfg = self._cfg
|
||||
api_key = cfg.get("api_key", "")
|
||||
@@ -542,6 +613,16 @@ class OpenAICompatBackend(LLMBackend):
|
||||
except Exception as e: # noqa: BLE001
|
||||
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:
|
||||
user_content: List[Any] = [
|
||||
{"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.raise_for_status()
|
||||
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"}
|
||||
resp = requests.post(url, json=payload, headers=headers, timeout=180, proxies=NO_PROXY)
|
||||
resp.raise_for_status()
|
||||
@@ -611,21 +693,20 @@ class OpenAICompatBackend(LLMBackend):
|
||||
"color_palette": str(d.get("color_palette", "")).strip(),
|
||||
"composition": str(d.get("composition", "")).strip(),
|
||||
"negative_prompt": str(d.get("negative_prompt", "")).strip(),
|
||||
"image_prompt": str(d.get("image_prompt", "")).strip(),
|
||||
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)
|
||||
"ref_images": [str(image_paths[i])] if i < len(image_paths) else [],
|
||||
"source": "pinterest",
|
||||
})
|
||||
return designs
|
||||
|
||||
def generate_title(self, image_path: str, system_prompt: str = "", country: str = "",
|
||||
fallback_text: str = "") -> Dict[str, Any]:
|
||||
"""多模态标题生成;图片输入不被模型支持(如 qwen 纯文本模型 400)时,
|
||||
自动降级为纯文本生成(fallback_text 为商品描述/热点主题)。"""
|
||||
def generate_title(self, image_path: str, system_prompt: str = "", country: str = "") -> Dict[str, Any]:
|
||||
"""多模态:分析服装图片,生成商品标题(按国家路由模板)。
|
||||
|
||||
系统提示词:显式传入优先;否则按 country 经 TITLE_TEMPLATE_ROUTE 路由到对应模板。
|
||||
模板 1(US/GB/AU/MX)返回 {"en_title","cn_title"};
|
||||
模板 2(JP)额外返回 {"ja_title"}。
|
||||
模板 2(JP)额外返回 {"ja_title"};
|
||||
模板 3(ES)返回 {"es_title","cn_title"}。
|
||||
无 key/调用失败返回 {}(调用方兜底不中断)。
|
||||
"""
|
||||
cfg = self._cfg
|
||||
@@ -678,6 +759,7 @@ class OpenAICompatBackend(LLMBackend):
|
||||
"en_title": str(parsed.get("en_title", "")).strip(),
|
||||
"cn_title": str(parsed.get("cn_title", "")).strip(),
|
||||
"ja_title": str(parsed.get("ja_title", "")).strip(),
|
||||
"es_title": str(parsed.get("es_title", "")).strip(),
|
||||
}
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[titles] 标题生成失败: {e}")
|
||||
|
||||
Reference in New Issue
Block a user