- {suite.images.map(img => (
+ {/* 只渲染终态(ok/failed)格子:生成中的 pending 占位不出现,避免"…/✗"占位格被误点 */}
+ {suite.images.filter(img => img.status === 'ok' || img.status === 'failed').map(img => (
{
) : (
)}
- {img.status !== 'ok' &&
{img.status === 'failed' ? '✗' : '…'}}
+ {img.status !== 'ok' &&
✗}
{img.status === 'failed' && img.error ? img.error : img.name}
diff --git a/extension/src/api/client.ts b/extension/src/api/client.ts
index cbb5f7e..6d1c3e6 100644
--- a/extension/src/api/client.ts
+++ b/extension/src/api/client.ts
@@ -4,7 +4,7 @@
*/
import type { ImageMaterial } from '../collector/scan';
-/** 服务端支持的套图类型(与 server/services/prompt.py 保持一致) */
+/** 服务端支持的套图类型(与 server/services/prompts/common.py 保持一致) */
export const SUITE_TYPE_OPTIONS = [
{ value: 'white_bg', label: '白底主图' },
{ value: 'key_features', label: '核心卖点图' },
diff --git a/server/api/generate.py b/server/api/generate.py
index 31c9977..2224aa3 100644
--- a/server/api/generate.py
+++ b/server/api/generate.py
@@ -11,7 +11,7 @@ from schemas import (
)
from services.generator import run_suite
from services.planner import generate_plan
-from services.prompt import type_name
+from services.prompts import type_name
from services.tasks import create_task
router = APIRouter(prefix="/api", tags=["generate"])
diff --git a/server/services/generator.py b/server/services/generator.py
index a3656e6..d74ec40 100644
--- a/server/services/generator.py
+++ b/server/services/generator.py
@@ -16,8 +16,8 @@ import httpx
from config import get_settings
from services import storage
-from services.prompt import build_prompt, build_context, type_name, wrap_prompt_for_gpt_edits
-from services.tasks import Task, TaskImage, TASK_FAILED, TASK_RUNNING, TASK_DONE, TASK_PARTIAL, IMG_OK
+from services.prompts import build_prompt, build_context, type_name
+from services.tasks import Task, TaskImage, TASK_FAILED, TASK_RUNNING, TASK_DONE, TASK_PARTIAL, IMG_FAILED, IMG_OK
log = logging.getLogger("suite.generator")
@@ -396,13 +396,11 @@ async def run_suite(task: Task) -> None:
image = TaskImage(type_id=type_id, name=job.get("title") or type_name(type_id))
task.images.append(image)
try:
+ # 提示词按模型家族分发:国产主体参考 / gpt edits 保真 / google 主体保持
prompt = build_prompt(
- type_id, ctx, task.style_set, task.lang,
+ provider_name, model, type_id, ctx, task.style_set, task.lang,
extra=job, style_prompt=task.style_prompt, requirements=task.requirements,
)
- # gpt-image edits 语义:商品冻结契约前置(含商品文字锚定),防止风格词改商品
- if provider_name == "rightapi":
- prompt = wrap_prompt_for_gpt_edits(prompt, ctx)
refs = _refs_for_job(list(task.ref_images or []), job)
data = await generator(prompt, refs, size=size, model=model)
# 部分中转不遵守 output_format(要 jpeg 回 PNG),按魔数定扩展名
@@ -413,6 +411,7 @@ async def run_suite(task: Task) -> None:
ok += 1
except Exception as exc: # noqa: BLE001
log.exception("套图 %s 类型 %s 生成失败", task.id, type_id)
+ image.status = IMG_FAILED # 默认 pending,失败显式置 failed
image.error = str(exc)[:500]
failures.append(f"{job.get('title') or type_name(type_id)}:{str(exc)[:200]}")
failed += 1
diff --git a/server/services/prompt.py b/server/services/prompt.py
deleted file mode 100644
index d7ecf08..0000000
--- a/server/services/prompt.py
+++ /dev/null
@@ -1,353 +0,0 @@
-"""套图 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)
diff --git a/server/services/prompts/__init__.py b/server/services/prompts/__init__.py
new file mode 100644
index 0000000..986277c
--- /dev/null
+++ b/server/services/prompts/__init__.py
@@ -0,0 +1,45 @@
+"""套图提示词引擎:按模型家族分发,各家族独立封装。
+
+不同家族的生图语义差异极大,共用一套提示词会导致语义错配
+(gpt-image-2 按文字重造商品即由此而来),故按家族各自成册:
+
+ alibaba 通义 wan*/qwen*(DashScope)—— 主体参考语义
+ doubao 豆包 Seedream(火山方舟)—— 主体参考语义,与通义共用装配
+ gpt gpt-image-2 / gpt-image-2-vip(RightAPI)—— /v1/images/edits 编辑语义
+ google nano-banana 系列(RightAPI)—— 原生主体保持语义
+
+路由规则:provider 为主;rightapi 内再按模型名细分 gpt / google。
+"""
+from __future__ import annotations
+
+from . import alibaba, doubao, google, gpt
+from .common import build_context, type_name
+
+_MODULE_BY_FAMILY = {
+ "alibaba": alibaba,
+ "doubao": doubao,
+ "gpt": gpt,
+ "google": google,
+}
+
+
+def prompt_family(provider: str, model: str | None) -> str:
+ """(provider, model) → 提示词家族名。"""
+ if provider == "rightapi":
+ if (model or "").lower().startswith("nano-banana"):
+ return "google"
+ return "gpt" # gpt-image-* 及未知中转模型默认按 edits 语义处理
+ if provider == "tongyi":
+ return "alibaba"
+ return "doubao" # doubao 及默认 provider
+
+
+def build_prompt(provider: str, model: str | None, 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。参数含义见各家族 build_prompt。"""
+ module = _MODULE_BY_FAMILY[prompt_family(provider, model)]
+ return module.build_prompt(
+ type_id, ctx, style_set, lang,
+ extra=extra, style_prompt=style_prompt, requirements=requirements,
+ )
diff --git a/server/services/prompts/alibaba.py b/server/services/prompts/alibaba.py
new file mode 100644
index 0000000..fc921a4
--- /dev/null
+++ b/server/services/prompts/alibaba.py
@@ -0,0 +1,169 @@
+"""阿里通义(wan* 万相 / qwen* 千问)提示词:国产"主体参考"语义。
+
+生图 API 把参考图当商品锚(subject reference)、prompt 当场景描述,
+风格词/文字商品描述不会反噬商品本体,负面清单也可以安全写入 prompt。
+豆包(doubao.py)与此语义一致,直接复用本模块装配。
+"""
+from __future__ import annotations
+
+from .common import (
+ STYLE_SETS, TEXT_RENDER, requirements_block, resolve_style, selling_point_lines,
+)
+
+# ── 公共组件(主体参考语义专用)────────────────────────────────────────────
+
+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."
+)
+
+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"
+)
+
+# ── 各图类型 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 = selling_point_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 = selling_point_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 = selling_point_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 = selling_point_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 最前面,
+ 声明覆盖一切冲突指令,用户可在此输入强制要求。
+ """
+ style = resolve_style(style_set, style_prompt)
+ 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}."
+ req = requirements_block(requirements)
+ if req:
+ prompt = f"{req} {prompt}"
+ return prompt + ". " + DEFAULT_NEGATIVE_INTENT
diff --git a/server/services/prompts/common.py b/server/services/prompts/common.py
new file mode 100644
index 0000000..6422354
--- /dev/null
+++ b/server/services/prompts/common.py
@@ -0,0 +1,154 @@
+"""提示词公共层:与模型家族无关的商品上下文、风格模板、图类型名与文案组件。
+
+各家族模块(alibaba / doubao / gpt / google)只负责"如何对模型说话",
+商品信息提炼与风格体系统一在这里维护,避免多处漂移。
+"""
+from __future__ import annotations
+
+import re
+
+# ── 风格模板(与插件端 STYLE_SET_OPTIONS 对应;提示词可被用户在插件里改写覆盖)───
+
+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": "创意图",
+}
+
+# ── 图内营销文案渲染规范(各家族共用;语言由平台决定)──────────────────────
+
+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."
+ ),
+}
+
+
+def resolve_style(style_set: int, style_prompt: str | None = None) -> dict:
+ """用户改写的风格提示词整体覆盖内置模板(tone/bg 整体替换)。"""
+ if style_prompt and style_prompt.strip():
+ return {"name": "custom", "tone": style_prompt.strip(), "bg": ""}
+ return STYLE_SETS.get(style_set, STYLE_SETS[1])
+
+
+def requirements_block(requirements: str | None) -> str:
+ """用户强制要求块:最高优先级、置于提示词最前、覆盖冲突指令(原文保留不翻译)。"""
+ if requirements and requirements.strip():
+ return (
+ "STRICT REQUIREMENTS (highest priority, must be followed exactly, "
+ "override any conflicting instruction): "
+ + requirements.strip().rstrip(".")
+ + "."
+ )
+ return ""
+
+
+# ── 商品上下文提炼 ────────────────────────────────────────────────────────
+
+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 selling_point_lines(ctx: dict, lang: str, max_n: int = 3) -> str:
+ """卖点列表 → 单行文案(图内 callout/标题用),无卖点返回空串。"""
+ 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))
+
+
+def type_name(type_id: str) -> str:
+ return TYPE_NAMES_ZH.get(type_id, type_id)
diff --git a/server/services/prompts/doubao.py b/server/services/prompts/doubao.py
new file mode 100644
index 0000000..eeea4c2
--- /dev/null
+++ b/server/services/prompts/doubao.py
@@ -0,0 +1,9 @@
+"""豆包(火山方舟 Seedream)提示词。
+
+豆包与通义同为国产"主体参考"生图模型:参考图即商品锚、prompt 为场景描述,
+提示词语义一致,直接复用阿里系装配;差异(去 AI 味后缀)在 generator 层追加。
+独立成文件便于后续按豆包特性分化。
+"""
+from __future__ import annotations
+
+from .alibaba import build_prompt as build_prompt # noqa: F401 主体参考语义与通义共用
diff --git a/server/services/prompts/google.py b/server/services/prompts/google.py
new file mode 100644
index 0000000..03cd3a0
--- /dev/null
+++ b/server/services/prompts/google.py
@@ -0,0 +1,163 @@
+"""Google 图像模型(nano-banana / nano-banana-2 / nano-banana-2-lite / nano-banana-pro)提示词。
+
+语义:Gemini 图像编辑 —— 原生主体保持能力强,输入图即"主体 + 底图",
+对自然语言指令遵循好。不套用 GPT 的编辑契约(冗长的拒绝条款反而稀释指令),
+也不用负面清单(无 negative_prompt 参数)。要点:
+ - 开头一句话钉死"主体 = 第一张图里的商品,逐像素保持";
+ - 指令自然语言描述目标画面(场景/排版/文案),不重述商品外观;
+ - 标题/参数仅作识别背景并声明以图为准。
+"""
+from __future__ import annotations
+
+from .common import TEXT_RENDER, requirements_block, resolve_style, selling_point_lines
+
+_SUBJECT_LOCK = (
+ "SUBJECT LOCK (highest priority): the product in the first image is the subject. "
+ "Keep it exactly as photographed — same shape, proportions, colors, print/pattern, "
+ "logo, label and every detail — and place that very product into the result. "
+ "A second image, when present, is another view of the same product for reference only."
+)
+
+_QUALITY = (
+ "OUTPUT: photorealistic commercial e-commerce photography, ultra-detailed, "
+ "natural light and shadow, professional retouching."
+)
+
+_REMINDER = (
+ "Reminder: keep the product exactly as in the first image; change only its surroundings, "
+ "composition, lighting and overlay graphics."
+)
+
+
+def _anchor(ctx: dict) -> str:
+ """商品文字锚定:仅供识别,明确以图为准(同 gpt 模块,避免文字反噬商品)。"""
+ line = f"Context (identification only): the product is \"{ctx['title']}\""
+ if ctx.get("params_line"):
+ line += f" ({ctx['params_line']})"
+ return line + ". The image, not this text, defines the product's appearance."
+
+
+# ── 各图类型指令(自然语言编辑口吻)────────────────────────────────────────
+
+def _task_white_bg(ctx: dict, lang: str) -> str:
+ return (
+ "Replace the background of this product photo with seamless pure white (RGB 255,255,255): "
+ "product centered in front view filling about 85% of the frame, even studio lighting with only "
+ "a faint natural contact shadow. No props, no added text, no background elements."
+ )
+
+def _task_key_features(ctx: dict, lang: str) -> str:
+ sp = selling_point_lines(ctx, lang) or ctx["title"]
+ return (
+ "Create a square key-features infographic: the unchanged product on the left two-thirds, "
+ "a clean right-hand panel with 3 feature callouts using minimal line icons and thin leader "
+ f"lines pointing at the product. Callout copy: {sp}."
+ )
+
+def _task_selling_pt(ctx: dict, lang: str) -> str:
+ sp = selling_point_lines(ctx, lang, 1) or ctx["title"]
+ return (
+ "Turn the photo into a single-selling-point poster: hero close-up of the unchanged product at "
+ f"a dynamic angle, one large bold headline about \"{sp}\", generous negative space, and a small "
+ "magnified circle zooming into an existing detail of the product."
+ )
+
+def _task_material(ctx: dict, lang: str) -> str:
+ return (
+ "Create an extreme macro close-up of an existing area of the product's surface, showing its "
+ "true fabric weave / texture / stitching exactly as in the photo; shallow depth of field, "
+ "raking light, small caption in a corner."
+ )
+
+def _task_lifestyle(ctx: dict, lang: str) -> str:
+ return (
+ "Place the unchanged product into a realistic everyday scene where it would naturally be used: "
+ "human-scale surroundings, soft daylight, authentic candid mood, the product as the clear visual focus."
+ )
+
+def _task_multi_scene(ctx: dict, lang: str) -> str:
+ sp = selling_point_lines(ctx, lang)
+ task = (
+ "Build a triptych of three vertical panels separated by thin gutters: each panel shows an "
+ "identical copy of the product in a different usage scene (home interior / outdoor street / "
+ "office desk), with consistent color grading across panels."
+ )
+ if sp:
+ task += f" Panel captions: {sp}."
+ return task
+
+def _task_ecommerce_detail(ctx: dict, lang: str) -> str:
+ sp = selling_point_lines(ctx, lang) or ctx["title"]
+ params = ctx["params_line"]
+ return (
+ "Compose a square detail-page hero section: top half a hero banner with the unchanged product "
+ "at a 3/4 angle; bottom half a clean spec card with 3 feature rows and line icons"
+ + (f" (specs: {params})" if params else "")
+ + f", one highlighted row: {sp}."
+ )
+
+def _task_size_chart(ctx: dict, lang: str) -> str:
+ dims = ctx["params_line"]
+ return (
+ "Create a size chart: the unchanged product in clean front and side views on a light background, "
+ "thin measurement annotation lines (arrows) marking length, width and height with values beside "
+ "each line"
+ + (f" (known specs: {dims})" if dims else "")
+ + ", small caption row, precise technical-drawing aesthetic."
+ )
+
+def _task_sku_collection(ctx: dict, lang: str) -> str:
+ # 不展开"全部配色":会凭空造出新商品;只排列同一件的多个副本
+ return (
+ "Arrange several identical copies of the product in a neat equal grid (2-4 per row) with a small "
+ "label chip below each copy; identical lighting and scale across copies. Every copy shows this "
+ "exact product — do not invent other colorways or variants."
+ )
+
+_TASK_BUILDERS = {
+ "white_bg": (_task_white_bg, False),
+ "key_features": (_task_key_features, True),
+ "selling_pt": (_task_selling_pt, True),
+ "material": (_task_material, True),
+ "lifestyle": (_task_lifestyle, True),
+ "multi_scene": (_task_multi_scene, True),
+ "ecommerce_detail": (_task_ecommerce_detail, True),
+ "size_chart": (_task_size_chart, True),
+ "sku_collection": (_task_sku_collection, True),
+}
+
+
+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:要求块 → 指令 → 主体锁 → 锚定 → 风格 → 文案 → 画质 → 提醒。"""
+ style = resolve_style(style_set, style_prompt)
+ extra = extra or {}
+ hint = (extra.get("prompt_hint") or "").strip()
+
+ if type_id == "custom":
+ purpose = extra.get("title") or ""
+ detail = extra.get("detail") or ""
+ task = "Create an e-commerce marketing image featuring the product from the first image"
+ task += f" — {purpose}" if purpose else ""
+ task += f": {detail}" if detail else ""
+ task += "."
+ wants_text = True
+ else:
+ entry = _TASK_BUILDERS.get(type_id)
+ if entry is None:
+ raise ValueError(f"未知图类型: {type_id}")
+ builder, wants_text = entry
+ task = builder(ctx, lang)
+ if hint:
+ task += f" Composition guidance: {hint}."
+
+ parts = [p for p in (requirements_block(requirements),) if p]
+ parts.append(task)
+ parts.append(_SUBJECT_LOCK)
+ parts.append(_anchor(ctx))
+ parts.append(f"Scene style (scene and background only, never the product): {style['tone']}.")
+ if wants_text:
+ parts.append(f"Text overlay (a graphic layer, never printed on the product): {TEXT_RENDER[lang]}")
+ parts.append(_QUALITY)
+ parts.append(_REMINDER)
+ return "\n\n".join(parts)
diff --git a/server/services/prompts/gpt.py b/server/services/prompts/gpt.py
new file mode 100644
index 0000000..5f8c64a
--- /dev/null
+++ b/server/services/prompts/gpt.py
@@ -0,0 +1,195 @@
+"""GPT 图像模型(gpt-image-2 / gpt-image-2-vip,RightAPI 中转)提示词。
+
+语义:/v1/images/edits —— 输入图是"被编辑的照片",prompt 是编辑指令;
+与通义/豆包的"主体参考"语义完全不同:参考图不是商品锚,模型会按文字指令
+重新渲染整张图。此前与国产模型共用场景提示词,再用文字锚定商品并要求输出
+"匹配商品描述",导致模型把商品改造成营销关键词描述的样子(必现商品被改)。
+
+本模块写法原则:
+ 1. 商品只由 Image 1 定义;标题/参数仅作识别背景并声明"以图为准",
+ 绝不要求输出匹配文字描述(那等于授权模型改商品);
+ 2. 指令只说"改什么"(背景/场景/排版/文案),不描述商品外观;
+ 3. 分节精简、首尾重申保真;不用负面清单(gpt 无 negative_prompt 参数,
+ 罗列畸形反而往上下文植入概念);
+ 4. sku 合集 / 多拼图明确"复制同一件商品,禁止发明新配色或变体"。
+"""
+from __future__ import annotations
+
+from .common import TEXT_RENDER, requirements_block, resolve_style, selling_point_lines
+
+# 保真锁:商品由 Image 1 唯一定义,其余指令一律不得触碰商品本体
+_PRESERVE = (
+ "PRESERVE (absolute, overrides every other instruction below): the product shown in Image 1. "
+ "Reuse the photographed product exactly as it is — identical shape, silhouette, proportions, "
+ "colors, print/pattern, logo and label text, materials, stitching and surface details. "
+ "Do not redesign, restyle, recolor, re-pattern, tidy up or substitute the product, "
+ "and do not let any style or text instruction below alter it. Image 2 is a secondary "
+ "view of the same product for reference only."
+)
+
+_STYLE = (
+ "SCENE STYLE (applies to background, scene, props and lighting only — never to the product): "
+)
+
+_QUALITY = (
+ "OUTPUT: photorealistic commercial e-commerce photography, ultra-detailed, "
+ "natural light and shadow, professional retouching."
+)
+
+_REMINDER = (
+ "FINAL CHECK: the product itself must remain exactly as photographed in Image 1 — "
+ "only its surroundings, composition, lighting and overlay graphics may differ."
+)
+
+# 图内文案:明确是"排版图层",不落在商品本体上
+_TEXT_SCOPE = (
+ "TEXT OVERLAY (a graphic layer on the composition, never printed on the product): "
+)
+
+
+def _anchor(ctx: dict) -> str:
+ """商品文字锚定:仅供识别,明确声明以图为准。
+
+ 只放标题 + 参数、不放营销描述——描述里的卖点词("卡通""加固""防水"等)
+ 在 edits 语义下会被执行到商品上;官逆通道(-vip)参考图被弱化时,
+ 文字锚定用于帮模型认出"是哪件商品",而不是"长什么样"。
+ """
+ line = f"CONTEXT (identification only): the product is \"{ctx['title']}\""
+ if ctx.get("params_line"):
+ line += f" ({ctx['params_line']})"
+ return (
+ line
+ + ". Image 1 — not this text — defines the product's appearance; "
+ "if they ever conflict, follow Image 1."
+ )
+
+
+# ── 各图类型的编辑指令(只描述改动,不描述商品)────────────────────────────
+
+def _task_white_bg(ctx: dict, lang: str) -> str:
+ return (
+ "TASK: Clean up this product photo for a marketplace listing. Replace the entire "
+ "background with seamless pure white (RGB 255,255,255); recompose with the product "
+ "centered in front view filling about 85% of the frame; keep only a faint natural "
+ "contact shadow. No props, no text, no background elements."
+ )
+
+def _task_key_features(ctx: dict, lang: str) -> str:
+ sp = selling_point_lines(ctx, lang) or ctx["title"]
+ return (
+ "TASK: Feature infographic on a square canvas. Keep the product unchanged on the left "
+ "two-thirds; build the right third as a clean info panel listing 3 feature callouts with "
+ f"minimal line icons and thin leader lines pointing at parts of the product. Callout copy: {sp}."
+ )
+
+def _task_selling_pt(ctx: dict, lang: str) -> str:
+ sp = selling_point_lines(ctx, lang, 1) or ctx["title"]
+ return (
+ "TASK: Single-selling-point poster. Hero close-up of the unchanged product at a dynamic "
+ f"angle, generous negative space, one large bold headline about \"{sp}\", plus one small "
+ "magnified circle zooming into an existing detail of the product (zoom only — do not "
+ "invent details that are not in the photo)."
+ )
+
+def _task_material(ctx: dict, lang: str) -> str:
+ return (
+ "TASK: Material close-up. Zoom tightly into an existing area of the product's surface and "
+ "show its true texture — fabric weave, surface finish, stitching — exactly as it appears in "
+ "Image 1; shallow depth of field, raking light across the surface; small caption label in a corner."
+ )
+
+def _task_lifestyle(ctx: dict, lang: str) -> str:
+ return (
+ "TASK: Lifestyle scene. Place the unchanged product into a realistic everyday environment "
+ "where it would naturally be used: human-scale surroundings, soft daylight, authentic candid "
+ "mood, matched shadows and color temperature, the product remaining the clear visual focus."
+ )
+
+def _task_multi_scene(ctx: dict, lang: str) -> str:
+ sp = selling_point_lines(ctx, lang)
+ task = (
+ "TASK: Triptych showcase. Build three vertical panels separated by thin gutters; every panel "
+ "contains an IDENTICAL copy of the product from Image 1 (do not re-render it differently per "
+ "panel) placed in a different usage scene (e.g. home interior / outdoor street / office desk), "
+ "with consistent color grading across panels."
+ )
+ if sp:
+ task += f" Panel captions: {sp}."
+ return task
+
+def _task_ecommerce_detail(ctx: dict, lang: str) -> str:
+ sp = selling_point_lines(ctx, lang) or ctx["title"]
+ params = ctx["params_line"]
+ return (
+ "TASK: Detail-page hero section on a square canvas. Top half: hero banner with the unchanged "
+ "product at a 3/4 angle. Bottom half: clean spec card with 3 feature rows and line icons"
+ + (f" (specs: {params})" if params else "")
+ + f", one highlighted row: {sp}."
+ )
+
+def _task_size_chart(ctx: dict, lang: str) -> str:
+ dims = ctx["params_line"]
+ return (
+ "TASK: Measurement chart. Show the unchanged product in clean front and side views on a light "
+ "background; overlay thin technical annotation lines (arrows) marking length, width and height "
+ "with measurement values rendered beside each line"
+ + (f" (known specs: {dims})" if dims else "")
+ + "; precise technical-drawing aesthetic, small caption row."
+ )
+
+def _task_sku_collection(ctx: dict, lang: str) -> str:
+ # 关键差异:不允许像国产模型那样展开"全部配色"——edits 语义下那会凭空造出新商品
+ return (
+ "TASK: Product lineup. Arrange several IDENTICAL copies of the product from Image 1 in a neat "
+ "equal grid (2-4 per row) with a small label chip below each copy; identical lighting and scale "
+ "across copies. Every copy must show this exact product — do NOT invent other colorways, "
+ "variants or versions."
+ )
+
+_TASK_BUILDERS = {
+ "white_bg": (_task_white_bg, False),
+ "key_features": (_task_key_features, True),
+ "selling_pt": (_task_selling_pt, True),
+ "material": (_task_material, True),
+ "lifestyle": (_task_lifestyle, True),
+ "multi_scene": (_task_multi_scene, True),
+ "ecommerce_detail": (_task_ecommerce_detail, True),
+ "size_chart": (_task_size_chart, True),
+ "sku_collection": (_task_sku_collection, True),
+}
+
+
+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:
+ """构造指定图类型的 edits 语义 prompt:要求块 → 编辑指令 → 保真锁 → 锚定 → 风格 → 文案 → 画质 → 终检。"""
+ style = resolve_style(style_set, style_prompt)
+ extra = extra or {}
+ hint = (extra.get("prompt_hint") or "").strip()
+
+ if type_id == "custom":
+ purpose = extra.get("title") or ""
+ detail = extra.get("detail") or ""
+ task = "TASK: Create an e-commerce marketing image featuring the product from Image 1"
+ task += f" — {purpose}" if purpose else ""
+ task += f": {detail}" if detail else ""
+ task += "."
+ wants_text = True
+ else:
+ entry = _TASK_BUILDERS.get(type_id)
+ if entry is None:
+ raise ValueError(f"未知图类型: {type_id}")
+ builder, wants_text = entry
+ task = builder(ctx, lang)
+ if hint:
+ task += f" Composition guidance: {hint}."
+
+ parts = [p for p in (requirements_block(requirements),) if p]
+ parts.append(task)
+ parts.append(_PRESERVE)
+ parts.append(_anchor(ctx))
+ parts.append(f"{_STYLE}{style['tone']}.")
+ if wants_text:
+ parts.append(f"{_TEXT_SCOPE}{TEXT_RENDER[lang]}")
+ parts.append(_QUALITY)
+ parts.append(_REMINDER)
+ return "\n\n".join(parts)
diff --git a/server/services/tasks.py b/server/services/tasks.py
index 93a7f41..287db14 100644
--- a/server/services/tasks.py
+++ b/server/services/tasks.py
@@ -16,6 +16,7 @@ TASK_PARTIAL = "partial"
TASK_FAILED = "failed"
# 任务内单张图状态
+IMG_PENDING = "pending" # 生成中(前端据此隐藏占位格,只渲染 ok/failed 终态)
IMG_OK = "ok"
IMG_FAILED = "failed"
@@ -26,7 +27,7 @@ class TaskImage:
type_id: str
name: str
- status: str = IMG_FAILED # 循环里先建后跑,成功后改为 ok
+ status: str = IMG_PENDING # 循环里先建后跑,成功改 ok、失败显式改 failed
url: str = ""
error: str | None = None