You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
ai-image/utils/mm_api_t2i.py

98 lines
4.1 KiB

"""GPT Image 和 Grok Image 文生图的核心逻辑。"""
import base64
import os
import time
import urllib.error
import urllib.request
from pathlib import Path
from uuid import uuid4
from fastapi import HTTPException
from openai import APIConnectionError, APIStatusError, APITimeoutError, OpenAI
API_BASE_URL = os.getenv("IMAGE_API_BASE_URL", "https://api.slomerex.xyz/v1")
OUTPUT_DIR = Path("output")
MAX_RETRIES = 3
RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
PROVIDERS = {
"grok": {
"api_key_env": "GROK_API_KEY",
"model": "grok-imagine-image-lite",
},
"gpt": {
"api_key_env": "GPT_API_KEY",
"model": "gpt-image-2-1K",
},
}
def _generate_with_retry(client: OpenAI, *, model: str, prompt: str, size: str, n: int):
"""调用图片接口,并对可恢复的上游错误进行指数退避重试。"""
for attempt in range(1, MAX_RETRIES + 1):
try:
return client.images.generate(model=model, prompt=prompt, size=size, n=n)
except APIStatusError as e:
if e.status_code not in RETRYABLE_STATUS_CODES or attempt == MAX_RETRIES:
raise HTTPException(status_code=502, detail=f"上游图片服务错误: HTTP {e.status_code}") from e
time.sleep(2 ** (attempt - 1))
except (APIConnectionError, APITimeoutError) as e:
if attempt == MAX_RETRIES:
raise HTTPException(status_code=504, detail="上游图片服务连接或请求超时") from e
time.sleep(2 ** (attempt - 1))
def _download_image(url: str) -> bytes:
"""从 URL 或 data URI 下载图片。"""
if url.startswith("data:image/"):
try:
_, encoded = url.split(",", 1)
return base64.b64decode(encoded, validate=True)
except (ValueError, TypeError) as e:
raise HTTPException(status_code=502, detail="图片 data URL 格式无效") from e
request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
for attempt in range(1, MAX_RETRIES + 1):
try:
with urllib.request.urlopen(request, timeout=120) as response:
return response.read()
except urllib.error.HTTPError as e:
if e.code not in RETRYABLE_STATUS_CODES or attempt == MAX_RETRIES:
raise HTTPException(status_code=502, detail=f"图片 URL 下载失败: HTTP {e.code}") from e
except (urllib.error.URLError, TimeoutError) as e:
if attempt == MAX_RETRIES:
raise HTTPException(status_code=504, detail="图片 URL 下载超时或网络连接失败") from e
time.sleep(2 ** (attempt - 1))
def generate_images(*, provider: str, model: str | None, prompt: str, size: str, n: int, profile_name: str):
"""生成图片并保存到本地,返回结果字典。"""
config = PROVIDERS[provider]
api_key = os.getenv(config["api_key_env"])
if not api_key:
raise HTTPException(status_code=500, detail=f"未配置环境变量 {config['api_key_env']}")
resolved_model = model or config["model"]
profile_dir = OUTPUT_DIR / profile_name
profile_dir.mkdir(parents=True, exist_ok=True)
client = OpenAI(base_url=API_BASE_URL, api_key=api_key, timeout=120.0, max_retries=0)
response = _generate_with_retry(client, model=resolved_model, prompt=prompt, size=size, n=n)
images = []
for item in response.data:
try:
if item.b64_json:
raw = base64.b64decode(item.b64_json, validate=True)
elif item.url:
raw = _download_image(item.url)
else:
raise HTTPException(status_code=502, detail="上游图片服务未返回 b64_json 或 url")
except ValueError as e:
raise HTTPException(status_code=502, detail="上游 Base64 图片数据格式无效") from e
filename = f"{resolved_model}-{uuid4().hex}.png"
(profile_dir / filename).write_bytes(raw)
images.append({"filename": filename, "url": f"/output/{profile_name}/{filename}", "bytes": len(raw)})
return {"provider": provider, "model": resolved_model, "images": images}