Files
image-suite-studio/server/services/prompt.py
T
2026-08-16 22:20:15 +08:00

324 lines
15 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": "",
},
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"
)
# ── 商品上下文提炼 ────────────────────────────────────────────────────────
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)