feat: 修改模型提示词

This commit is contained in:
R524809
2026-08-20 12:36:00 +08:00
parent 6732cb178a
commit 90b7c8737d
14 changed files with 899 additions and 366 deletions
+6 -2
View File
@@ -45,8 +45,12 @@ Chrome 插件(WXT + React + antd Python 后端(FastAPI,无数
### 套图生成(server/services
- `prompt.py`:7 种图类型 × 5 套风格模板,公共组件 QUALITY / PRODUCT_REF_LOCK(商品一致性锁)/ TEXT_RENDER
- 图类型:白底主图 / 核心卖点图 / 卖点图 / 材质图 / 场景展示图 / 多场景拼图 / 电商详情图
- `services/prompts/`:提示词按模型家族独立封装,`__init__.py` 按 (provider, model) 路由分发
- `common.py`:商品上下文提炼、5 套风格模板、图内文案规范 TEXT_RENDER(家族共用)
- `alibaba.py`:通义 wan*/qwen*(主体参考语义);`doubao.py`:豆包(同语义,复用阿里装配)
- `gpt.py`gpt-image-2/-vip`/v1/images/edits` 编辑语义,保真优先:商品只由 Image 1 定义,文字锚定仅作识别)
- `google.py`nano-banana 系列(原生主体保持语义)
- 图类型:白底主图 / 核心卖点图 / 卖点图 / 材质图 / 场景展示图 / 多场景拼图 / 电商详情图 / 尺寸标注图 / SKU合集 / 创意图
- 风格:北欧极简 / 清新明亮 / 高级感深色 / 暖调生活 / 纯净棚拍
- 卖点从采集的参数表/卖点文本自动提炼
- `generator.py`:图像 provider(图生图,参考图 = 采集主图)
@@ -0,0 +1,146 @@
# RightAPIgpt-image / nano-banana)调用方式排查报告
> 2026-08-20 · 状态:**待确认**(确认后再改代码)
> 结论先行:**是调用方式不对**。现行代码把参考图用 multipart 传给未在文档中的
> `/v1/images/edits` 端点;该中转已于 2026-07-14 全面切换"统一异步模式",文档中的
> 正确用法是 `/v1/images/generations` + JSON `image`data-URI 数组)+ `async: true`
> 提交任务,再轮询 `/v1/tasks/{task_id}` 取图。实测:**文档路径下 gpt-image-2、
> gpt-image-2-vip、nano-banana-2-lite 全部逐像素保真**;现行 edits 路径要么 502、
> 要么出图但参考图未生效(商品按文字重造)。
---
## 1. 现行代码怎么调的(generator.py `_rightapi_request`
```python
# 有参考图(套图流程必然有)→ multipart POST /v1/images/edits
files = [("image[]", ("ref-1.png", data, mime)), ...]
data = {"model", "prompt", "size": "2048x2048", "quality": "high",
"output_format": "jpeg", "n": 1, "input_fidelity": "high"}
resp = client.post(f"{base}/v1/images/edits", files=files, data=data)
# 期望同步响应 data[0].b64_json / data[0].url,无任务轮询
```
问题:
- **`/v1/images/edits` 不在文档接口列表里**(文档只有:图片生成、Gemini 生成、任务查询);
- 参考图用 multipart `image[]` 传输——新管道只认 JSON body 里的 `image`data-URI 数组);
- 未带 `"async": true`,也没有任务轮询逻辑;
- `quality` / `output_format` / `input_fidelity` 均不在文档参数表中。
## 2. 文档的正确用法(docs.rightapi.ai2026-07-14 更新)
### 2.1 提交:POST `/v1/images/generations`OpenAI Images 兼容,异步)
```json
{
"model": "gpt-image-2", // 或 nano-banana 系列等
"prompt": "...",
"n": 1,
"size": "1:1", // 比例 1:1 / 16:9 / 9:16 / 4:3,或像素串 "1024x1024"
"async": true, // 固定带
"image": ["data:image/png;base64,..."] // 参考图:data-URI 数组(保真关键)
}
```
响应(立即返回):
```json
{"task_id": "task_xxx", "status": "processing", "progress": 0, "message": "..."}
```
### 2.2 轮询:GET `/v1/tasks/{task_id}`(站点级,**不带 /draw 前缀**)
- 进行中:`{"id","task_id","object","model","status":"in_progress","progress":0~2,"created_at"}`
- **完成:`{"created": ..., "data": [{"url": "https://...jpeg"}]}`**
——实测完成响应**没有 `status: "completed"` 字段**(与文档描述不符),
完成判定 = 响应里出现 `data`;结果只有 `url`(未见 b64_json)。
- `progress` 基本不动(一直 0~2),只能当装饰,不能当进度条依据。
### 2.3 其他要点
- Gemini 原生端点 `/v1beta/models/{model}:generateContent`contents/parts + inline_data
generationConfig.imageConfig 支持 aspectRatio / imageSize)——nano-banana 系列可走,
但非必需(generations 端点同样支持传参考图),本期可不做;
- `imageSize`"1K"/"2K"/"4K"**仅 nano-banana / gpt-image vip 模型可用**
- 文档域名示例为 `www.right.codes/draw`,实测现有配置 `rightapi.ai/draw` 仍通
(提交与任务查询都可用,`rightapi.ai/v1/tasks/...` 实测正常)。
## 3. 实测证据(2026-08-20,受控对照实验)
测试图:程序生成的特征图形——白底 + 青色杯身 + 红色横条纹 + 三颗黄色五角星 + 右侧把手。
提示词:"把背景替换成纯绿色,保持图中那个青色杯子完全不变……"。
保真判定 = 逐项核对杯身/条纹/星星/把手是否原样(我人工查看生成图)。
| # | 路径 | 模型 | 结果 |
|---|------|------|------|
| A | **文档路径** generations + image[] + async | nano-banana-2-lite | ✅ **保真完美**,仅背景变绿 |
| C | **文档路径** generations + image[] + async | gpt-image-2 | ✅ **保真完美**,仅背景变绿 |
| D | **文档路径** generations + image[] + async | gpt-image-2-vip(官逆) | ✅ **保真完美**,仅背景变绿 |
| B | **现行代码** edits + multipart image[] | nano-banana-2-lite | ❌ **502 Bad Gateway**(间隔 90s 重试仍 502;同期 generations 路径正常) |
用户今日实测(11:08–11:16,本地任务表,同一鲨鱼玩偶参考图):
| 套图 | 模型(路径) | 结果 |
|------|--------------|------|
| 7bd57ffa | gpt-image-2-vip(现行 edits | ⚠️ 出图,但鲨鱼被**重新设计**(眼睛/鱼鳍/比例全变) |
| ee36e92f | nano-banana-2(现行 edits | ⚠️ 同上,商品被重造 |
| 42c37fa2 | wan2.6-imageDashScope,正常链路) | ⚠️ 鲨鱼同样有漂移(**另一层问题**,见 §5) |
探针产物(供复核):`/tmp/rightapi-probe/`ref.png / gen-async-lite.png / gen-async-0.png)。
## 4. 根因分析
1. **参考图从未真正送达模型**edits + multipart 是旧同步模式的调用方式;中转 7-14
切到统一异步管道后,multipart 参考图不被解析 → 模型只收到 prompt 文字 → 按文字
(含标题/风格词)重新合成商品 → **"不是原商品"必现**。gpt 与 google 全中,因为
它们共用这一条错误链路。
2. **端点本身进入半废弃状态**:今天 edits 已对 lite 模型直接 502(两次、间隔 90s),
对 gpt-image-2-vip / nano-banana-2 尚能返回(用户 11 点实测出图)——属于残留兼容,
随时可能全断。之前代码里"同 key 分钟级冷却 502"的注释,与该端点的不稳定状态吻合。
3. 提示词层面的修复(上一轮 gpt/google 家族重写)方向正确但**没治病根**:参考图没到
模型,提示词写得再保真也没用。证据:同一套提示词组件,走文档路径(探测 A/C/D)
保真完美。
## 5. 顺带观察:通义今日也有漂移(不在本次修复范围)
wan2.6-image 走 DashScope 正常链路(参考图确实送达)仍重造了鲨鱼——这是
主体参考模型能力/提示词层面的问题(wan2.6-image 是参考遵循较弱的一档),
与本次 RightAPI 调用方式无关,建议后续单独评估(比如套餐默认模型换成
wan2.7-image-pro 或 qwen-image-3.0-pro,两者参考遵循更强)。
## 6. 修复方案(确认后实施)
只改 `server/services/generator.py` 的 RightAPI provider,提示词层不动:
1. **统一走 `/v1/images/generations`**(有无参考图都走它;无参考图就不带 `image` 字段):
```python
body = {"model": model, "prompt": prompt, "n": 1,
"size": size, "async": True}
if refs:
body["image"] = [data_uri, ...] # data-URI 数组(≤2 张,沿用现选图逻辑)
resp = post(f"{base}/v1/images/generations", json=body)
task_id = resp.json()["task_id"]
```
2. **新增任务轮询**`GET {origin}/v1/tasks/{task_id}`origin = base 去掉 `/draw`);
3s 起步、逐步加到 10s,上限沿用 `poll_max_wait`600s,gpt 高质量单张 1–5 分钟);
完成判定 = `data` 出现(不能依赖 `status == "completed"`);失败态 = `status` 为
failed/error/cancelled;然后下载 `data[0].url`。
3. **参数清理**:删 `quality` / `output_format` / `input_fidelity`(均非文档参数;
`input_fidelity` 的探测-降级机制整体移除)。`size` 改传像素串
`"1536x2048"`3:4/ `"2048x2048"`(1:1)——比例枚举里没有 3:4,像素串是文档允许的写法。
4. **重试保留**:提交/轮询遇到 429/5xx/超时,沿用 60→120→240s 退避(`rightapi_max_retries`)。
5. **配置**`RIGHTAPI_BASE_URL` 保持 `https://rightapi.ai/draw` 不变;`rightapi_image_quality`
配置项删除(或停用)。
预计工作量:`_rightapi_request` 重写约 60 行 + 轮询函数 30 行,其余层(提示词分发、
任务执行器、前端)零改动。
## 7. 上线前待确认项
1. **3:4 像素串 `1536x2048` 是否被接受**——探测只验证了 `size: "1:1"`(文档说像素串
合法,但建议改完后先出 1 张 Ozon 规格图验证);
2. nano-banana / nano-banana-2 / nano-banana-pro 三个型号未逐一实测(同族接口一致,
lite / gpt 系已验证通路,风险低);
3. 是否启用 `imageSize`2K/4K,仅 nano-banana 与 gpt-image vip 支持)——默认不传,
需要高清再说;
4. Gemini 原生端点(`:generateContent`)本期不接,留作后续选项。
+3 -2
View File
@@ -894,7 +894,8 @@ const App: React.FC = () => {
{' '}· {STYLE_SET_OPTIONS.find(s => s.value === suite.style_set)?.label ?? '自定义'}
</div>
<div className="result-grid">
{suite.images.map(img => (
{/* 只渲染终态(ok/failed)格子:生成中的 pending 占位不出现,避免"…/✗"占位格被误点 */}
{suite.images.filter(img => img.status === 'ok' || img.status === 'failed').map(img => (
<div
key={img.type_id + img.name}
className={`result-cell ${img.status !== 'ok' ? 'fail' : ''}`}
@@ -907,7 +908,7 @@ const App: React.FC = () => {
) : (
<div style={{ aspectRatio: suite.ratio === '3:4' ? '3/4' : '1', background: 'var(--card-soft)' }} />
)}
{img.status !== 'ok' && <span className="fail-tag">{img.status === 'failed' ? '✗' : '…'}</span>}
{img.status !== 'ok' && <span className="fail-tag"></span>}
<div className="cap" title={img.error || img.name}>
{img.status === 'failed' && img.error ? img.error : img.name}
</div>
+1 -1
View File
@@ -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: '核心卖点图' },
+1 -1
View File
@@ -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"])
+5 -6
View File
@@ -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
-353
View File
@@ -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·]+|(?<!\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)
+45
View File
@@ -0,0 +1,45 @@
"""套图提示词引擎:按模型家族分发,各家族独立封装。
不同家族的生图语义差异极大,共用一套提示词会导致语义错配
gpt-image-2 按文字重造商品即由此而来),故按家族各自成册:
alibaba 通义 wan*/qwen*DashScope)—— 主体参考语义
doubao 豆包 Seedream(火山方舟)—— 主体参考语义,与通义共用装配
gpt gpt-image-2 / gpt-image-2-vipRightAPI)—— /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,
)
+169
View File
@@ -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
+154
View File
@@ -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·]+|(?<!\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 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)
+9
View File
@@ -0,0 +1,9 @@
"""豆包(火山方舟 Seedream)提示词。
豆包与通义同为国产"主体参考"生图模型:参考图即商品锚、prompt 为场景描述,
提示词语义一致,直接复用阿里系装配;差异(去 AI 味后缀)在 generator 层追加。
独立成文件便于后续按豆包特性分化。
"""
from __future__ import annotations
from .alibaba import build_prompt as build_prompt # noqa: F401 主体参考语义与通义共用
+163
View File
@@ -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)
+195
View File
@@ -0,0 +1,195 @@
"""GPT 图像模型(gpt-image-2 / gpt-image-2-vipRightAPI 中转)提示词。
语义:/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)
+2 -1
View File
@@ -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