feat: 增加gpt-image-2 模型

This commit is contained in:
R524809
2026-08-17 17:28:02 +08:00
parent 9d15d8f784
commit e9d1eef07e
11 changed files with 162 additions and 17 deletions
+7
View File
@@ -27,6 +27,13 @@ DASHSCOPE_API_KEY=
DASHSCOPE_BASE_URL=
DASHSCOPE_MODEL=wan2.7-image-pro
# --- RightAPIgpt-imageOpenAI 兼容中转)---
RIGHTAPI_API_KEY=
RIGHTAPI_BASE_URL=https://rightapi.ai/draw
RIGHTAPI_IMAGE_MODEL=gpt-image-2
# 出图质量:auto | low | medium | highhigh 单张约 1-5 分钟,超时自动兜底 600s)
RIGHTAPI_IMAGE_QUALITY=high
# --- DeepSeek(出图方案规划器)---
DEEPSEEK_API_KEY=
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
+2
View File
@@ -41,6 +41,8 @@ Chrome 插件(WXT + React + antd Python 后端(FastAPI + SQLite
- `generator.py`:图像 provider(图生图,参考图 = 采集主图)
- `doubao`:火山方舟 Seedream(默认,`ARK_API_KEY`
- `tongyi`:通义万相/千问(`DASHSCOPE_API_KEY`wan* 异步轮询 / qwen* 同步)
- `rightapi`gpt-image-2OpenAI 兼容中转 `RIGHTAPI_API_KEY`edits 参考图 / generations 同步)
- 插件只传模型名,服务端按模型名自动路由到对应 provider
## 快速开始
+35
View File
@@ -0,0 +1,35 @@
# ===== 服务 =====
# 注意:不要用 5000/7000(macOS 隔空播放接收器占用)
HOST=127.0.0.1
PORT=3300
# 拼给插件/前端的媒体访问地址(默认本机)
APP_BASE_URL=http://127.0.0.1:3300
# ===== 存储 =====
# 图片/数据库落盘目录(默认 <项目根>/data
# DATA_DIR=/Users/joey/sites/seller-store/image-suite-studio/data
# ===== 图像生成 =====
# 默认 providerdoubao(火山方舟 Seedream| tongyi(阿里 DashScope| rightapigpt-image
IMAGE_PROVIDER=doubao
# 单张生图请求超时(秒)
REQUEST_TIMEOUT=300
# 异步任务轮询上限(秒)
POLL_MAX_WAIT=600
# --- 豆包 / 火山方舟 ---
ARK_API_KEY=
ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3/images/generations
ARK_IMAGE_MODEL=doubao-seedream-4-5-251128
# --- 通义 / DashScope ---
DASHSCOPE_API_KEY=
DASHSCOPE_BASE_URL=
DASHSCOPE_MODEL=wan2.7-image-pro
# --- RightAPIgpt-imageOpenAI 兼容中转)---
RIGHTAPI_API_KEY=sk-b6b9ffba28b64ca795be31f6dee842c6
RIGHTAPI_BASE_URL=https://rightapi.ai/draw
RIGHTAPI_IMAGE_MODEL=gpt-image-2
# 出图质量:auto | low | medium | highhigh 单张约 1-5 分钟,超时自动兜底 600s)
RIGHTAPI_IMAGE_QUALITY=high
+2 -1
View File
@@ -20,5 +20,6 @@
"@types/react-dom": "^18.3.0",
"typescript": "^5.5.3",
"wxt": "^0.19.0"
}
},
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
}
+6 -1
View File
@@ -102,7 +102,7 @@ export const PLATFORM_OPTIONS = [
export type PlatformId = (typeof PLATFORM_OPTIONS)[number]['value'];
/** 生图模型(通义 DashScope):下拉可选 + 中文特点说明 */
/** 生图模型(下拉可选 + 中文特点说明;服务端按模型名路由 provider) */
export const IMAGE_MODEL_OPTIONS = [
{
value: 'qwen-image-3.0-pro',
@@ -114,6 +114,11 @@ export const IMAGE_MODEL_OPTIONS = [
label: 'wan2.7-image-pro',
desc: '异步精修,质感与细节更强,适合高质量电商大片',
},
{
value: 'gpt-image-2',
label: 'gpt-image-2',
desc: 'GPT 图像模型,构图与图内文案渲染最强,参考图高保真,单张 1-5 分钟',
},
] as const;
export const PLATFORM_SPECS: Record<string, { lang: string; ratio: string; label: string }> = {
+6 -2
View File
@@ -7,7 +7,8 @@ from config import get_settings
from db import get_db
from models import Suite
from schemas import (
GenerateRequest, PLATFORM_SPECS, SUPPORTED_TYPES, TONGYI_MODELS, SuiteCreateResponse, TextMaterial,
GenerateRequest, PLATFORM_SPECS, SUPPORTED_TYPES, TONGYI_MODELS, RIGHTAPI_MODELS,
SuiteCreateResponse, TextMaterial, resolve_provider,
PlanRequest, PlanResponse, PlanItemOut,
)
from services.generator import run_suite
@@ -84,10 +85,13 @@ async def generate_suite(
spec = PLATFORM_SPECS[req.platform]
settings = get_settings()
provider_name = req.provider or settings.image_provider
# 插件只传模型名:已知模型直接路由到对应 providergpt-image-2 → rightapi
provider_name = resolve_provider(req.model, req.provider, settings.image_provider)
model = req.model
if provider_name == "tongyi" and model and model not in TONGYI_MODELS:
raise HTTPException(status_code=400, detail=f"不支持的模型: {model}tongyi 支持: {TONGYI_MODELS}")
if provider_name == "rightapi" and model and model not in RIGHTAPI_MODELS:
raise HTTPException(status_code=400, detail=f"不支持的模型: {model}rightapi 支持: {RIGHTAPI_MODELS}")
suite = Suite(
product_id=None,
style_set=req.style_set,
+8 -2
View File
@@ -13,7 +13,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import get_settings
from db import get_db
from models import Product, ProductAsset, Suite, SuiteImage, STATUS_OK
from schemas import PLATFORM_SPECS, SUPPORTED_TYPES, TONGYI_MODELS, SuiteCreateRequest, SuiteCreateResponse, SuiteImageOut, SuiteOut
from schemas import (
PLATFORM_SPECS, SUPPORTED_TYPES, TONGYI_MODELS, RIGHTAPI_MODELS,
SuiteCreateRequest, SuiteCreateResponse, SuiteImageOut, SuiteOut, resolve_provider,
)
from services import storage
from services.generator import run_suite
@@ -76,9 +79,12 @@ async def create_suite(
spec = PLATFORM_SPECS[req.platform]
settings = get_settings()
provider_name = req.provider or settings.image_provider
# 模型名优先路由:已知模型直接定位 providergpt-image-2 → rightapi
provider_name = resolve_provider(req.model, req.provider, settings.image_provider)
if provider_name == "tongyi" and req.model and req.model not in TONGYI_MODELS:
raise HTTPException(status_code=400, detail=f"不支持的模型: {req.model}tongyi 支持: {TONGYI_MODELS}")
if provider_name == "rightapi" and req.model and req.model not in RIGHTAPI_MODELS:
raise HTTPException(status_code=400, detail=f"不支持的模型: {req.model}rightapi 支持: {RIGHTAPI_MODELS}")
suite = Suite(
product_id=product.id,
style_set=req.style_set,
+6
View File
@@ -41,6 +41,12 @@ class Settings(BaseSettings):
dashscope_base_url: str = "" # 留空按模型自动选择万象异步/千问同步端点
dashscope_model: str = "wan2.7-image-pro"
# RightAPIOpenAI 兼容中转,gpt-image 系列)
rightapi_api_key: str = ""
rightapi_base_url: str = "https://rightapi.ai/draw"
rightapi_image_model: str = "gpt-image-2"
rightapi_image_quality: str = "high" # auto | low | medium | high
# DeepSeek(出图方案规划器)
deepseek_api_key: str = ""
deepseek_base_url: str = "https://api.deepseek.com/v1"
+1
View File
@@ -50,6 +50,7 @@ async def health():
"provider": settings.image_provider,
"ark_configured": bool(settings.ark_api_key),
"dashscope_configured": bool(settings.dashscope_api_key),
"rightapi_configured": bool(settings.rightapi_api_key),
}
+16
View File
@@ -12,6 +12,22 @@ SUPPORTED_TYPES = [
# 通义(DashScope)生图模型白名单:插件下拉可选的模型
TONGYI_MODELS = ["qwen-image-3.0-pro", "wan2.7-image-pro"]
# RightAPI 生图模型白名单
RIGHTAPI_MODELS = ["gpt-image-2"]
# 模型 → provider 推断表:插件只传模型名,服务端据此路由(模型名优先于 provider 字段)
MODEL_PROVIDERS: dict[str, str] = {
**{m: "tongyi" for m in TONGYI_MODELS},
**{m: "rightapi" for m in RIGHTAPI_MODELS},
}
def resolve_provider(model: str | None, requested: str | None, default: str) -> str:
"""已知模型名直接定位 provider;未知模型回退到请求指定的 provider 或默认值。"""
if model and model in MODEL_PROVIDERS:
return MODEL_PROVIDERS[model]
return requested or default
# ── 采集上传 ──
+73 -11
View File
@@ -46,6 +46,9 @@ def _image_size(provider: str, ratio: str, is_wan: bool = True) -> str:
"""平台比例 → provider 尺寸参数。3:4 竖版(Ozon/WB),1:1 方图(国内)。"""
if provider == "doubao":
return "1536x2048" if ratio == "3:4" else "2048x2048"
if provider == "rightapi":
# gpt-image 自定义尺寸约束:16 的倍数、长短边比 ≤ 3:1(1536x2048 合法)
return "1536x2048" if ratio == "3:4" else "2048x2048"
# tongyi:万象与千问的 size 语法相同(* 分隔),档位不同
if ratio == "3:4":
return "1536*2048" if is_wan else "768*1024"
@@ -70,27 +73,34 @@ def _bytes_to_data_uri(data: bytes, mime: str) -> str:
return f"data:{mime};base64,{base64.b64encode(data).decode()}"
async def _resolve_ref(url: str) -> str:
"""参考图 URL → data URI。本地 media 文件直读磁盘;远程 URL 带 Referer 下载。
async def _resolve_ref_bytes(url: str) -> tuple[bytes, str]:
"""参考图 URL → (bytes, mime)。本地 media 文件直读磁盘;远程 URL 带 Referer 下载。
生图 API 的服务器无法访问 127.0.0.1,代理 URL 也不能直接透传,
所以统一在本地解析成 base64 data URI 再进请求体
所以统一在本地解析成原始字节再进请求体(data URI 或 multipart
"""
if url.startswith("data:"):
return url
head, _, b64 = url.partition(",")
mime = head[5:].split(";", 1)[0] or "image/jpeg"
return base64.b64decode(b64), mime
path = storage.local_path(url)
if path is not None:
mime = mimetypes.guess_type(path.name)[0] or "image/jpeg"
return _bytes_to_data_uri(path.read_bytes(), mime)
return path.read_bytes(), mime
if url.startswith(("http://", "https://")):
from api.proxy import guess_referer
data, ctype = await storage.download_bytes(url, referer=guess_referer(url))
if not ctype.startswith("image/"):
ctype = "image/jpeg"
return _bytes_to_data_uri(data, ctype)
return data, ctype
raise FileNotFoundError(f"无法解析参考图: {url}")
async def _resolve_ref(url: str) -> str:
data, mime = await _resolve_ref_bytes(url)
return _bytes_to_data_uri(data, mime)
# ── Provider:豆包 Seedream(火山方舟)────────────────────────────────────
async def generate_doubao(prompt: str, ref_images: list[str], size: str = "2048x2048", model: str | None = None) -> bytes:
@@ -198,7 +208,56 @@ async def generate_tongyi(prompt: str, ref_images: list[str], size: str = "2048*
return dl.content
GENERATORS = {"doubao": generate_doubao, "tongyi": generate_tongyi}
# ── ProviderRightAPIgpt-imageOpenAI 兼容中转)──────────────────────
async def generate_rightapi(prompt: str, ref_images: list[str], size: str = "2048x2048", model: str | None = None) -> bytes:
"""gpt-image 系列:有参考图走 /v1/images/editsmultipart),无参考图走 /v1/images/generations。
OpenAI 兼容协议:响应固定 b64_json(不支持 response_format 参数,传了报 400);
同步调用无任务轮询,高质量档单张 1-5 分钟,超时按文档建议兜底 600s。
"""
s = get_settings()
if not s.rightapi_api_key:
raise RuntimeError("未配置 RIGHTAPI_API_KEY.env")
model = model or s.rightapi_image_model
base = s.rightapi_base_url.rstrip("/")
headers = {"Authorization": f"Bearer {s.rightapi_api_key}"}
async with httpx.AsyncClient(timeout=max(s.request_timeout, 600), verify=False) as client:
common = {
"model": model,
"prompt": prompt,
"size": size,
"quality": s.rightapi_image_quality,
"output_format": "jpeg", # 与落盘 .jpg 后缀一致
"n": 1,
}
if ref_images:
files = []
for i, u in enumerate(ref_images):
data, mime = await _resolve_ref_bytes(u)
files.append(("image[]", (f"ref-{i + 1}.{mime.split('/')[-1]}", data, mime)))
resp = await client.post(f"{base}/v1/images/edits", headers=headers, files=files, data=common)
else:
resp = await client.post(
f"{base}/v1/images/generations",
headers={**headers, "Content-Type": "application/json"},
json=common,
)
_raise_api_error(resp, "RightAPI")
item = resp.json()["data"][0]
b64 = item.get("b64_json") or ""
if b64:
return base64.b64decode(b64.split(",", 1)[-1] if "," in b64 else b64)
img_url = item.get("url") or ""
if not img_url:
raise RuntimeError(f"RightAPI 响应里没有图片数据: {item}")
dl = await client.get(img_url, timeout=s.request_timeout)
dl.raise_for_status()
return dl.content
GENERATORS = {"doubao": generate_doubao, "tongyi": generate_tongyi, "rightapi": generate_rightapi}
# ── 任务执行器 ────────────────────────────────────────────────────────────
@@ -280,9 +339,10 @@ async def run_suite(suite_id: str) -> None:
raw = suite.context if not product else (product.raw or {})
ctx = build_context(raw or {}, fallback_name=product.name if product else "")
model = suite.model or (
settings.dashscope_model if provider_name == "tongyi" else settings.ark_image_model
)
model = suite.model or {
"tongyi": settings.dashscope_model,
"rightapi": settings.rightapi_image_model,
}.get(provider_name, settings.ark_image_model)
is_wan = provider_name == "tongyi" and _is_wan_model(model)
size = _image_size(provider_name, suite.ratio, is_wan=is_wan)
@@ -317,7 +377,9 @@ async def run_suite(suite_id: str) -> None:
else:
refs = _refs_for_job(list(suite.ref_images or []), job)
data = await generator(prompt, refs, size=size, model=model)
key = storage.write_bytes(data, key_prefix=f"suites/{suite.id}", ext=".jpg")
# 部分中转不遵守 output_format(要 jpeg 回 PNG),按魔数定扩展名
ext = ".png" if data[:8] == b"\x89PNG\r\n\x1a\n" else ".jpg"
key = storage.write_bytes(data, key_prefix=f"suites/{suite.id}", ext=ext)
image_row.stored_url = storage.public_url(key)
image_row.status = STATUS_OK
ok += 1