feat: 增加gpt-image-2 模型
This commit is contained in:
@@ -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}
|
||||
# ── Provider:RightAPI(gpt-image,OpenAI 兼容中转)──────────────────────
|
||||
|
||||
async def generate_rightapi(prompt: str, ref_images: list[str], size: str = "2048x2048", model: str | None = None) -> bytes:
|
||||
"""gpt-image 系列:有参考图走 /v1/images/edits(multipart),无参考图走 /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
|
||||
|
||||
Reference in New Issue
Block a user