357 lines
17 KiB
Python
357 lines
17 KiB
Python
"""套图 Prompt 引擎。
|
||
|
||
借鉴 ecommerce-image-suite 的动态 Prompt 架构,浓缩为:
|
||
- 7 种图类型 × 5 套视觉风格模板
|
||
- 公共组件:QUALITY(画质)/ PRODUCT_REF_LOCK(商品一致性锁)/ TEXT_RENDER(图内文案规范)
|
||
- 卖点从采集的参数表/卖点文本自动提炼
|
||
|
||
核心原则:所有图严格保持商品一致性(same silhouette, same print, same color),
|
||
只允许改变背景 / 角度 / 光线 / 排版。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
# ── 风格模板(与插件端 STYLE_SET_OPTIONS 对应;提示词可被用户在插件里改写覆盖)───
|
||
# 提示词用中文:生图 provider(通义万相/豆包)均为国产模型,中文理解一流,且便于用户自行改写。
|
||
|
||
STYLE_SETS: dict[int, dict] = {
|
||
1: {
|
||
"name": "高级质感大片",
|
||
"tone": "高端电商大片质感,柔和的方向性棚拍光,背景带细腻的浅渐变,材质纹理清晰可见,"
|
||
"色彩层次高级克制,商业画册级品质,构图干净、留白充足",
|
||
"bg": "",
|
||
},
|
||
2: {
|
||
"name": "清新生活场景",
|
||
"tone": "明亮通透的生活场景摄影,自然窗光,柔和的低饱和居家环境,浅景深虚化,"
|
||
"真实自然的氛围感,绿植与暖色织物点缀,温馨有人气",
|
||
"bg": "",
|
||
},
|
||
3: {
|
||
"name": "极简白底规范",
|
||
"tone": "极简棚拍风格,纯净无缝的浅色背景,柔和均匀的无影布光,以商品为中心的严谨构图,"
|
||
"安静的高级感,画面只保留轻微的自然接触投影",
|
||
"bg": "",
|
||
},
|
||
4: {
|
||
"name": "炫彩促销风",
|
||
"tone": "高能量促销风格,高饱和度色块背景搭配动感几何图形,强对比,节日大促海报氛围,"
|
||
"构图抢眼、视觉冲击力强",
|
||
"bg": "",
|
||
},
|
||
5: {
|
||
"name": "暗调轻奢",
|
||
"tone": "暗调轻奢质感,深炭灰色背景,轮廓光勾勒商品边缘,材质细节丰富,带轻微雾感,"
|
||
"如美术馆展陈般的呈现",
|
||
"bg": "",
|
||
},
|
||
6: {
|
||
"name": "俄式风情",
|
||
"tone": "俄式风情电商大片,浓郁温暖的色调,红与金的传统配色点缀,冬日节庆氛围,"
|
||
"深色木质与毛毡织物背景,如暖炉烛光般的柔和光晕,厚重扎实的质感,"
|
||
"带一丝巴洛克式的华丽细节,适合俄语区市场",
|
||
"bg": "",
|
||
},
|
||
7: {
|
||
"name": "北欧极简",
|
||
"tone": "北欧极简风格,白色与浅灰的原木空间,大量自然漫射光,干净利落的线条,"
|
||
"浅色木质背景点缀少量绿植,克制的中性配色,画面通透轻盈,"
|
||
"舒适宁静的氛围",
|
||
"bg": "",
|
||
},
|
||
}
|
||
|
||
# ── 图类型中文名(导出文件名用)───────────────────────────────────────────
|
||
|
||
TYPE_NAMES_ZH: dict[str, str] = {
|
||
"white_bg": "白底主图",
|
||
"key_features": "核心卖点图",
|
||
"selling_pt": "卖点图",
|
||
"material": "材质图",
|
||
"lifestyle": "场景展示图",
|
||
"multi_scene": "多场景拼图",
|
||
"ecommerce_detail": "电商详情图",
|
||
"size_chart": "尺寸标注图",
|
||
"sku_collection": "SKU合集图",
|
||
"custom": "创意图",
|
||
}
|
||
|
||
# ── 公共组件 ──────────────────────────────────────────────────────────────
|
||
|
||
QUALITY = (
|
||
"Shot on Sony A7R V with 85mm lens at f/2.0, ultra-detailed, photorealistic, "
|
||
"8K commercial image quality, professional retouching."
|
||
)
|
||
|
||
PRODUCT_REF_LOCK = (
|
||
"CRITICAL: The product must look EXACTLY the same as in the reference image — "
|
||
"identical silhouette, proportions, colors, print pattern, stitching and every design detail. "
|
||
"Only the background, camera angle, lighting and styling may change. "
|
||
"Do not redesign, add or remove any element of the product."
|
||
)
|
||
|
||
TEXT_RENDER = {
|
||
"zh": (
|
||
"Render concise Chinese marketing text inside the image: main headline max 8 Chinese characters, "
|
||
"sub-lines max 12 characters each, font is modern clean sans-serif (Source Han Sans style), "
|
||
"high legibility, tasteful typography layout, colors harmonized with the composition. "
|
||
"No spelling errors, no garbled characters."
|
||
),
|
||
"en": (
|
||
"Render concise English marketing text inside the image: headline max 5 words, "
|
||
"sub-lines max 8 words each, Helvetica Neue style sans-serif, high legibility, "
|
||
"tasteful typography layout, colors harmonized with the composition. No spelling errors."
|
||
),
|
||
"ru": (
|
||
"Render concise Russian marketing text inside the image: headline max 4 words, "
|
||
"sub-lines max 6 words each, modern clean sans-serif (Inter / PT Sans style), "
|
||
"proper Cyrillic typography, high legibility, tasteful layout, colors harmonized with the composition. "
|
||
"No spelling errors, no mixed latin/cyrillic gibberish."
|
||
),
|
||
}
|
||
|
||
DEFAULT_NEGATIVE_INTENT = (
|
||
"no AI-generated look, no CGI quality, no plastic appearance, no watermark, "
|
||
"no distorted text, no deformed product, no extra limbs, no blurry areas"
|
||
)
|
||
|
||
# ── gpt-image(/v1/images/edits 语义)专用包装 ─────────────────────────────
|
||
# gpt-image 的 edits 端点把输入图当"被编辑的底图"、prompt 当"编辑指令"(豆包/通义则是
|
||
# "主体参考"),风格词会被字面执行到商品上。按 OpenAI 官方提示词指南的编辑模式:
|
||
# 按序号说明输入图、PRESERVE/MAY CHANGE 分列、首尾重申不变量、文案逐字渲染。
|
||
|
||
GPT_EDITS_CONTRACT = (
|
||
"INPUT IMAGES: Image 1 (and Image 2 if present) are reference photos of ONE product "
|
||
"from different angles. Use them ONLY as the source of the product's true appearance.\n"
|
||
"PRESERVE (frozen, never change): the product itself — silhouette, proportions, colors, "
|
||
"print/pattern (keep stripes / logos / labels exactly), materials, texture, stitching, "
|
||
"hardware and every design detail. The product in the output must be the same physical "
|
||
"item as in the input images, merely photographed in a new setting.\n"
|
||
"MAY CHANGE: background, scene, props, camera angle, lighting, composition "
|
||
"and in-image marketing typography.\n"
|
||
"STYLE SCOPE: all style, mood, color-palette and decoration instructions below describe "
|
||
"the SCENE AND BACKGROUND ONLY — never apply them to the product itself. "
|
||
"Do not restyle, recolor, re-pattern or redecorate the product. "
|
||
"You may relight the product so it sits naturally in the new scene "
|
||
"(matched shadows and color temperature), but never change its design, colors or pattern."
|
||
)
|
||
|
||
GPT_EDITS_FINAL_CHECK = (
|
||
"FINAL CHECK before output: if the product in your result differs from the input product in any "
|
||
"design detail (shape, color, pattern, material, logo), the image is rejected. "
|
||
"Render any listed marketing copy / headlines exactly as written (verbatim, no extra characters, "
|
||
"no paraphrasing)."
|
||
)
|
||
|
||
|
||
def wrap_prompt_for_gpt_edits(prompt: str) -> str:
|
||
"""gpt-image edits 语义适配:契约放开头(指令权重最高处),终检放结尾。"""
|
||
return f"{GPT_EDITS_CONTRACT}\n\n{prompt}\n\n{GPT_EDITS_FINAL_CHECK}"
|
||
|
||
# ── 商品上下文提炼 ────────────────────────────────────────────────────────
|
||
|
||
def _shorten(text: str, n: int) -> str:
|
||
text = re.sub(r"\s+", " ", (text or "")).strip()
|
||
return text[:n]
|
||
|
||
def _clean_title(title: str) -> str:
|
||
"""去掉常见堆砌词,让标题更可读。"""
|
||
t = _shorten(title, 60)
|
||
return re.sub(r"[【【】】\\[\\]|/]", " ", t).strip()
|
||
|
||
def build_context(raw: dict, fallback_name: str = "", fallback_desc: str = "") -> dict:
|
||
"""从采集数据提炼生图上下文:标题、描述行、卖点列表、参数行。
|
||
|
||
raw: {title, desc, price, params: [{key, value}], sellingPoints}
|
||
"""
|
||
title = _clean_title(raw.get("title") or fallback_name or "product")
|
||
desc = _shorten(raw.get("desc") or fallback_desc or "", 200)
|
||
|
||
# 卖点:优先显式卖点文本;否则从参数表里挑短而有信息量的键值对
|
||
selling_points: list[dict] = []
|
||
sp_text = raw.get("sellingPoints") or ""
|
||
if sp_text:
|
||
for chunk in re.split(r"[;;\n·]+|(?<!\d)\.(?!\d)", sp_text):
|
||
c = _shorten(chunk, 20)
|
||
if c and len(selling_points) < 5:
|
||
selling_points.append({"zh": c, "en": c})
|
||
if not selling_points:
|
||
for p in (raw.get("params") or [])[:12]:
|
||
k, v = _shorten(p.get("key", ""), 10), _shorten(str(p.get("value", "")), 16)
|
||
if k and v and k.lower() not in {"货号", "sku", "isbn", "上架时间"}:
|
||
selling_points.append({"zh": f"{k} {v}", "en": f"{k} {v}"})
|
||
if len(selling_points) >= 5:
|
||
break
|
||
|
||
params_line = "; ".join(
|
||
f"{p.get('key')}: {p.get('value')}" for p in (raw.get("params") or [])[:8]
|
||
)
|
||
return {
|
||
"title": title,
|
||
"title_en": title, # 采集源多为中文标题,英文场景直接用原词避免乱翻译
|
||
"desc": desc,
|
||
"selling_points": selling_points[:3],
|
||
"params_line": params_line,
|
||
"price": raw.get("price") or "",
|
||
}
|
||
|
||
def _sp_lines(ctx: dict, lang: str, max_n: int = 3) -> str:
|
||
sps = ctx["selling_points"][:max_n]
|
||
if not sps:
|
||
return ""
|
||
key = "zh" if lang == "zh" else "en"
|
||
return "; ".join(s[key] for s in sps if s.get(key))
|
||
|
||
# ── 各图类型 Prompt ───────────────────────────────────────────────────────
|
||
|
||
def _prompt_white_bg(ctx: dict, style: dict, lang: str) -> str:
|
||
return (
|
||
f"E-commerce main product image on pure white background (RGB 255,255,255), "
|
||
f"product \"{ctx['title']}\" centered and filling about 85% of the frame, "
|
||
f"front view, even shadowless studio lighting with a faint natural contact shadow, "
|
||
f"{style['tone']}. No text, no props, no background elements. {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_key_features(ctx: dict, style: dict, lang: str) -> str:
|
||
sp = _sp_lines(ctx, lang) or ctx["title"]
|
||
return (
|
||
f"E-commerce key-features infographic for product \"{ctx['title']}\", square layout: "
|
||
f"product on the left two-thirds ({style['bg']}), right column lists 3 feature callouts "
|
||
f"with minimal line icons, thin leader lines pointing to product details. "
|
||
f"Feature callouts: {sp}. {style['tone']}. {TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_selling_pt(ctx: dict, style: dict, lang: str) -> str:
|
||
sp = _sp_lines(ctx, lang, 1) or ctx["title"]
|
||
return (
|
||
f"Single-selling-point e-commerce poster for product \"{ctx['title']}\": "
|
||
f"hero product close-up at dynamic angle ({style['bg']}), one large bold headline "
|
||
f"about \"{sp}\", generous negative space, one small magnified detail circle "
|
||
f"highlighting material or craft. {style['tone']}. {TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_material(ctx: dict, style: dict, lang: str) -> str:
|
||
return (
|
||
f"Macro material close-up of product \"{ctx['title']}\": extreme detail shot revealing "
|
||
f"fabric weave / surface texture / stitching / finish, shallow depth of field, "
|
||
f"raking light across the surface, {style['tone']}. Small caption label in corner. "
|
||
f"{TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_lifestyle(ctx: dict, style: dict, lang: str) -> str:
|
||
bg = f" ({style['bg']})" if style.get("bg") else ""
|
||
return (
|
||
f"Lifestyle in-context scene for product \"{ctx['title']}\": the product is naturally "
|
||
f"used / placed in a real environment{bg}, realistic human-scale surroundings, "
|
||
f"soft daylight, authentic candid mood, product remains the clear visual focus. "
|
||
f"{style['tone']}. {TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_multi_scene(ctx: dict, style: dict, lang: str) -> str:
|
||
sp = _sp_lines(ctx, lang)
|
||
return (
|
||
f"Triptych multi-scene e-commerce image for product \"{ctx['title']}\": three vertical panels "
|
||
f"separated by thin gutters, each panel shows the SAME product in a different usage scene "
|
||
f"(e.g. home interior / outdoor street / office desk), consistent color grading across panels. "
|
||
f"Panel captions: {sp}. {style['tone']}. {TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_ecommerce_detail(ctx: dict, style: dict, lang: str) -> str:
|
||
sp = _sp_lines(ctx, lang) or ctx["title"]
|
||
params = ctx["params_line"]
|
||
return (
|
||
f"E-commerce detail-page hero section for product \"{ctx['title']}\", square layout: "
|
||
f"top half is a hero banner with the product at a 3/4 angle ({style['bg']}); "
|
||
f"bottom half is a clean spec card listing 3 feature rows with line icons"
|
||
+ (f" (specs: {params})" if params else "")
|
||
+ f" and one highlighted row: {sp}. {style['tone']}. {TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_size_chart(ctx: dict, style: dict, lang: str) -> str:
|
||
dims = ctx["params_line"]
|
||
return (
|
||
f"Product size chart infographic for \"{ctx['title']}\": product shown in clean front and side views "
|
||
f"on light background, with thin measurement annotation lines (arrows) marking length, width and height, "
|
||
f"measurement values rendered next to each line"
|
||
+ (f" (known specs: {dims})" if dims else "")
|
||
+ f", small caption row, precise technical drawing aesthetic. {style['tone']}. "
|
||
f"{TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_sku_collection(ctx: dict, style: dict, lang: str) -> str:
|
||
return (
|
||
f"Colorway collection image for product \"{ctx['title']}\": the SAME product in all its color/variant "
|
||
f"options arranged in a neat equal grid (2-4 items per row), each colorway with a small label chip below it, "
|
||
f"consistent lighting and scale across all items, clean e-commerce presentation. "
|
||
f"{style['tone']}. {TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
)
|
||
|
||
def _prompt_custom(ctx: dict, style: dict, lang: str, extra: dict) -> str:
|
||
hint = (extra.get("prompt_hint") or "").strip()
|
||
purpose = extra.get("title") or ""
|
||
detail = extra.get("detail") or ""
|
||
bg = f" {style['bg']} as environment." if style.get("bg") else ""
|
||
composed = (
|
||
f"E-commerce marketing image for product \"{ctx['title']}\""
|
||
+ (f" — {purpose}" if purpose else "")
|
||
+ (f": {detail}" if detail else "")
|
||
+ "."
|
||
)
|
||
if hint:
|
||
composed += f" Composition: {hint}."
|
||
return f"{composed}{bg} {style['tone']}. {TEXT_RENDER[lang]} {QUALITY} {PRODUCT_REF_LOCK}"
|
||
|
||
_PROMPT_BUILDERS = {
|
||
"white_bg": _prompt_white_bg,
|
||
"key_features": _prompt_key_features,
|
||
"selling_pt": _prompt_selling_pt,
|
||
"material": _prompt_material,
|
||
"lifestyle": _prompt_lifestyle,
|
||
"multi_scene": _prompt_multi_scene,
|
||
"ecommerce_detail": _prompt_ecommerce_detail,
|
||
"size_chart": _prompt_size_chart,
|
||
"sku_collection": _prompt_sku_collection,
|
||
}
|
||
|
||
|
||
def build_prompt(type_id: str, ctx: dict, style_set: int, lang: str, extra: dict | None = None,
|
||
style_prompt: str | None = None, requirements: str | None = None) -> str:
|
||
"""构造指定图类型的完整生图 prompt。
|
||
|
||
extra: 方案项信息 {title, detail, prompt_hint}——custom 类型必需,
|
||
预设类型也会把 prompt_hint 作为构图补充注入。
|
||
style_prompt: 用户改写的风格提示词,覆盖 style_set 内置模板(tone/bg 整体替换)。
|
||
requirements: 生图要求(最高优先级,强制约束),置于 prompt 最前面,
|
||
声明覆盖一切冲突指令,用户可在此输入强制要求。
|
||
"""
|
||
if style_prompt and style_prompt.strip():
|
||
style = {"name": "custom", "tone": style_prompt.strip(), "bg": ""}
|
||
else:
|
||
style = STYLE_SETS.get(style_set, STYLE_SETS[1])
|
||
extra = extra or {}
|
||
if type_id == "custom":
|
||
prompt = _prompt_custom(ctx, style, lang, extra)
|
||
else:
|
||
builder = _PROMPT_BUILDERS.get(type_id)
|
||
if builder is None:
|
||
raise ValueError(f"未知图类型: {type_id}")
|
||
prompt = builder(ctx, style, lang)
|
||
hint = (extra.get("prompt_hint") or "").strip()
|
||
if hint:
|
||
prompt = prompt.rstrip(".") + f". Additional composition guidance: {hint}."
|
||
# 生图要求:最高优先级,置于最前并声明覆盖冲突指令(用户输入原样保留,不翻译)
|
||
if requirements and requirements.strip():
|
||
prompt = (
|
||
"STRICT REQUIREMENTS (highest priority, must be followed exactly, "
|
||
"override any conflicting instruction): "
|
||
+ requirements.strip().rstrip(".")
|
||
+ ". "
|
||
+ prompt
|
||
)
|
||
return prompt + ". " + DEFAULT_NEGATIVE_INTENT
|
||
|
||
|
||
def type_name(type_id: str) -> str:
|
||
return TYPE_NAMES_ZH.get(type_id, type_id)
|