Files
image-suite-studio/server/services/prompt.py
T

354 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""套图 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": "",
},
}
# ── 图类型中文名(导出文件名用)───────────────────────────────────────────
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 分列、首尾重申不变量、文案逐字渲染。
def gpt_edits_contract(ctx: dict) -> str:
"""gpt-image edits 语义契约(放开头,指令权重最高处)。
除通用锁定条款外,注入商品文字锚定(标题 + 关键参数 + 描述):
官逆通道(gpt-image-2-vip 等)会把参考图当对话附件弱化处理,
input_fidelity 类 API 参数不生效,此时商品文字描述是保真的唯一兜底。
"""
anchor = f" The product is: \"{ctx['title']}\""
if ctx.get("params_line"):
anchor += f" (key specs: {ctx['params_line']})"
if ctx.get("desc"):
anchor += f". {ctx['desc']}"
return (
"TASK: Edit the attached product photos — re-photograph THE SAME physical product "
"in a new setting. This is an edit of the input images, NOT a new product design.\n"
"INPUT IMAGES: Image 1 = product front view (PRIMARY source of truth for the product's "
f"true appearance); Image 2 (if present) = product back / detail view.{anchor}\n"
"PRODUCT LOCK (highest priority, overrides everything else in this prompt): exactly "
"preserve the product's shape, silhouette, proportions, colors, label text, logos, "
"print/pattern, materials and texture. Do not redesign, restyle, recolor, re-pattern "
"or substitute the product with a similar one. The output must show the very product "
"from the input images AND match the product description above; if the generated "
"product differs from either in any design detail, the image is rejected.\n"
"MAY CHANGE: background, scene, props, camera angle, lighting, composition "
"and in-image marketing typography only. You may relight the product so it sits "
"naturally in the new scene (matched shadows and color temperature).\n"
"STYLE SCOPE: all style, mood, color-palette and decoration instructions below describe "
"the SCENE AND BACKGROUND ONLY — never apply them to the product. "
"When any style instruction conflicts with product fidelity, product fidelity always wins."
)
GPT_EDITS_FINAL_CHECK = (
"FINAL CHECK before output: if the product in your result differs from the input product "
"or the product description above 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, ctx: dict) -> str:
"""gpt-image edits 语义适配:契约放开头(指令权重最高处),终检放结尾。"""
return f"{gpt_edits_contract(ctx)}\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)