diff --git a/README.md b/README.md index dd0f084..703cd06 100644 --- a/README.md +++ b/README.md @@ -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(图生图,参考图 = 采集主图) diff --git a/docs/rightapi-调用排查与修复方案.md b/docs/rightapi-调用排查与修复方案.md new file mode 100644 index 0000000..88ac779 --- /dev/null +++ b/docs/rightapi-调用排查与修复方案.md @@ -0,0 +1,146 @@ +# RightAPI(gpt-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.ai,2026-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-image(DashScope,正常链路) | ⚠️ 鲨鱼同样有漂移(**另一层问题**,见 §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`)本期不接,留作后续选项。 diff --git a/extension/entrypoints/sidepanel/App.tsx b/extension/entrypoints/sidepanel/App.tsx index 98f729b..7d0fd15 100644 --- a/extension/entrypoints/sidepanel/App.tsx +++ b/extension/entrypoints/sidepanel/App.tsx @@ -894,7 +894,8 @@ const App: React.FC = () => { {' '}· 风格「{STYLE_SET_OPTIONS.find(s => s.value === suite.style_set)?.label ?? '自定义'}」
- {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