feat: AI 生图、图片下载优化
This commit is contained in:
@@ -61,9 +61,11 @@ def _raise_api_error(resp, provider: str):
|
||||
raise ApiError(f"{provider} API HTTP {resp.status_code} — {detail}", resp.status_code)
|
||||
try:
|
||||
body = resp.json()
|
||||
detail = f"{body.get('code', '')}: {body.get('message', '')}".strip(': ')
|
||||
except Exception: # noqa: BLE001
|
||||
detail = text[:200]
|
||||
raise ApiError(f"{provider} API HTTP {resp.status_code} — {text[:200]}", resp.status_code)
|
||||
# 兼容两种错误形状:顶层 {code, message}(豆包/通义)与嵌套 {error: {code, message}}(new_api 系中转)
|
||||
err = body.get("error") if isinstance(body.get("error"), dict) else body
|
||||
detail = f"{err.get('code', '')}: {err.get('message', '')}".strip(': ')
|
||||
raise ApiError(f"{provider} API HTTP {resp.status_code} — {detail or '无错误详情'}", resp.status_code)
|
||||
|
||||
# 参考图选择:material 用第 2 张(背面/细节),其余用第 1 张(正面)
|
||||
@@ -80,6 +82,9 @@ def _image_size(provider: str, ratio: str, is_wan: bool = True, model: str = "")
|
||||
if provider == "rightapi":
|
||||
# gpt-image 自定义尺寸约束:16 的倍数、长短边比 ≤ 3:1(1536x2048 合法)
|
||||
return "1536x2048" if ratio == "3:4" else "2048x2048"
|
||||
if provider == "pcore":
|
||||
# pcore 的 size 是枚举(默认 1024x1024),不接受自定义尺寸;用 gpt-image 枚举:竖版 1024x1536(近似 3:4)
|
||||
return "1024x1536" if ratio == "3:4" else "1024x1024"
|
||||
# tongyi:万象与千问的 size 语法相同(* 分隔),档位不同
|
||||
# wan2.6 系列总像素限制在 [1280², 1440²],wan2.7 的 1536*2048/2048*2048 会超限
|
||||
if model.startswith("wan2.6"):
|
||||
@@ -379,7 +384,110 @@ async def generate_rightapi(prompt: str, ref_images: list[str], size: str = "204
|
||||
raise last_exc # type: ignore[misc]
|
||||
|
||||
|
||||
GENERATORS = {"doubao": generate_doubao, "tongyi": generate_tongyi, "rightapi": generate_rightapi}
|
||||
# ── Provider:Pcore(pcore.ai,OpenAI 兼容异步图片协议)──────────────────
|
||||
|
||||
async def _pcore_poll_task(client: httpx.AsyncClient, headers: dict, base: str,
|
||||
kind: str, task_id: str, max_wait: int) -> dict:
|
||||
"""轮询 Pcore 任务:GET {base}/v1/images/{kind}/{task_id},kind ∈ generations | edits。
|
||||
|
||||
状态机:queued / in_progress → 继续;completed / succeeded → 返回;
|
||||
failed / error / cancelled → 抛错。
|
||||
"""
|
||||
poll_url = f"{base}/v1/images/{kind}/{task_id}"
|
||||
elapsed, interval = 0, 3
|
||||
while elapsed < max_wait:
|
||||
resp = await client.get(poll_url, headers=headers, timeout=30)
|
||||
_raise_api_error(resp, "Pcore")
|
||||
result = resp.json()
|
||||
status = result.get("status", "")
|
||||
if status in ("failed", "error", "cancelled"):
|
||||
err = result.get("error") or {}
|
||||
raise RuntimeError(f"Pcore 任务失败: {err.get('message') or result}")
|
||||
if status in ("completed", "succeeded"):
|
||||
return result
|
||||
await asyncio.sleep(interval)
|
||||
elapsed += interval
|
||||
interval = min(interval + 2, 10)
|
||||
raise TimeoutError(f"Pcore 异步任务超时 ({max_wait}s): task_id={task_id}")
|
||||
|
||||
|
||||
async def _pcore_request(s, prompt: str, ref_images: list[str], size: str, model: str) -> bytes:
|
||||
"""Pcore:无参考图走 /v1/images/generations,带参考图走 /v1/images/edits(JSON,data-URI)。
|
||||
|
||||
两者均为异步:提交返回任务 id → 轮询对应资源路径 → 完成后 data[0].url 取图。
|
||||
完成形状与 RightAPI 同为 Images 协议,复用 _rightapi_extract_image。
|
||||
"""
|
||||
base = s.pcore_base_url.rstrip("/")
|
||||
headers = {"Authorization": f"Bearer {s.pcore_api_key}"}
|
||||
|
||||
kind = "edits" if ref_images else "generations"
|
||||
body: dict = {"model": model, "prompt": prompt, "n": 1, "size": size, "async": True}
|
||||
if ref_images:
|
||||
# pcore 的 edits 要求 images 为 [{image_url: ...}] 对象数组(实测纯字符串报 images[].image_url is required)
|
||||
body["images"] = [{"image_url": await _resolve_ref(u)} for u in ref_images]
|
||||
|
||||
async with httpx.AsyncClient(timeout=max(s.request_timeout, 600), verify=False) as client:
|
||||
resp = await client.post(
|
||||
f"{base}/v1/images/{kind}",
|
||||
headers={**headers, "Content-Type": "application/json"},
|
||||
json=body,
|
||||
)
|
||||
_raise_api_error(resp, "Pcore")
|
||||
submitted = resp.json()
|
||||
# new_api 系中转常见「HTTP 200 + 错误体」,需主动检查 error 字段
|
||||
if isinstance(submitted, dict) and submitted.get("error"):
|
||||
err = submitted["error"] or {}
|
||||
raise RuntimeError(f"Pcore 提交失败: {err.get('message') or submitted}")
|
||||
task_id = submitted.get("task_id") or submitted.get("id") or ""
|
||||
if not task_id:
|
||||
raise RuntimeError(f"Pcore 提交未返回任务 id: {submitted}")
|
||||
result = await _pcore_poll_task(client, headers, base, kind, task_id, s.poll_max_wait)
|
||||
|
||||
img_kind, payload = _rightapi_extract_image(result)
|
||||
if img_kind == "b64" and payload:
|
||||
return base64.b64decode(payload.split(",", 1)[-1] if "," in payload else payload)
|
||||
if img_kind == "url" and payload:
|
||||
dl = await client.get(payload, timeout=s.request_timeout)
|
||||
dl.raise_for_status()
|
||||
return dl.content
|
||||
raise RuntimeError(f"Pcore 任务完成但没有图片数据: {result}")
|
||||
|
||||
|
||||
async def generate_pcore(prompt: str, ref_images: list[str], size: str = "1536x2048", model: str | None = None) -> bytes:
|
||||
"""带重试的 Pcore 入口:429/5xx/超时按递增间隔重试(与 RightAPI 同策略)。"""
|
||||
s = get_settings()
|
||||
if not s.pcore_api_key:
|
||||
raise RuntimeError("未配置 PCORE_API_KEY(.env)")
|
||||
model = model or "gpt-image-2"
|
||||
attempts = max(1, s.pcore_max_retries)
|
||||
|
||||
last_exc: Exception | None = None
|
||||
for i in range(attempts):
|
||||
try:
|
||||
return await _pcore_request(s, prompt, ref_images, size, model)
|
||||
except ApiError as exc:
|
||||
last_exc = exc
|
||||
if exc.status not in RETRYABLE_STATUS:
|
||||
raise
|
||||
# 通道不存在(模型未开通)重试无意义,立即失败
|
||||
if "no available channel" in str(exc).lower() or "model_not_found" in str(exc).lower():
|
||||
raise
|
||||
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
||||
last_exc = exc # 网络抖动/超时可重试
|
||||
if i == attempts - 1:
|
||||
break
|
||||
wait = s.pcore_retry_wait * (2 ** i)
|
||||
log.warning("Pcore 第 %d/%d 次请求失败(%s),%ds 后重试", i + 1, attempts, last_exc, wait)
|
||||
await asyncio.sleep(wait)
|
||||
raise last_exc # type: ignore[misc]
|
||||
|
||||
|
||||
GENERATORS = {
|
||||
"doubao": generate_doubao,
|
||||
"tongyi": generate_tongyi,
|
||||
"rightapi": generate_rightapi,
|
||||
"pcore": generate_pcore,
|
||||
}
|
||||
|
||||
|
||||
# ── 任务执行器 ────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user