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
+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