"""套图 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·]+|(?= 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)