- 新增 BatchAsyncImageGenerator 节点(全并发+即时落盘,不怕中途失败丢图) - 原版 AsyncImageGenerator 移除批量提示词功能,单节点只处理单提示词 - 模型改名:限时特价→次卡,gemini→nano-banana-官方 - 异步节点过滤 官方计费 渠道,仅保留次卡和官方模型 - 单任务超时提升至900s,批量超时改为动态计算(批次数×900s) - No available channel for model 错误转化为中文友好提示 - 新增 base_async_provider / gemini_async_provider 异步客户端基类 Co-Authored-By: Claude Opus 4.6 <[email protected]>
150 lines
5.4 KiB
Python
150 lines
5.4 KiB
Python
"""
|
||
Gemini 异步生图 Provider
|
||
通过 cf-api.o1key.com 的异步提交+轮询接口调用 Gemini 图像生成模型
|
||
|
||
协议说明:
|
||
- 提交:POST {base}/async{gemini_endpoint}?image_format=url
|
||
- 轮询:GET {base}/async/v1/tasks/{task_id}
|
||
- 结果:可能直接返回 image_url,也可能返回 Gemini 标准 candidates 格式
|
||
"""
|
||
|
||
from io import BytesIO
|
||
from typing import Dict, List, Optional
|
||
from PIL import Image
|
||
|
||
from .base_async_provider import BaseAsyncImageProvider
|
||
from .gemini_client import GeminiAPIClient
|
||
from ..utils.config import get_async_api_base_url, get_api_key_or_raise
|
||
from ..models_config import (
|
||
get_enabled_models,
|
||
get_model_supported_aspect_ratios,
|
||
get_model_supported_resolutions,
|
||
)
|
||
|
||
|
||
class GeminiAsyncImageProvider(BaseAsyncImageProvider):
|
||
"""
|
||
Gemini 异步生图 Provider
|
||
|
||
委托 GeminiAPIClient 处理:
|
||
- 端点构造(get_endpoint)
|
||
- 请求体构建(build_request_body)
|
||
- 响应解析(parse_response_async)
|
||
"""
|
||
|
||
def __init__(self, api_key: str = None, proxy_url: str = None):
|
||
if api_key is None:
|
||
api_key = get_api_key_or_raise("O1KEY_API_KEY")
|
||
super().__init__(api_key=api_key, proxy_url=proxy_url)
|
||
self._client = GeminiAPIClient(api_key=api_key)
|
||
|
||
# ========================================================================
|
||
# 抽象方法实现
|
||
# ========================================================================
|
||
|
||
@property
|
||
def api_base_url(self) -> str:
|
||
return get_async_api_base_url()
|
||
|
||
def get_submit_endpoint(self, model: str, resolution: str) -> str:
|
||
gemini_endpoint = self._client.get_endpoint(
|
||
model=model, resolution=resolution, image_format="url"
|
||
)
|
||
base = gemini_endpoint.split("?")[0]
|
||
async_endpoint = f"/async{base}"
|
||
if "?" in gemini_endpoint:
|
||
async_endpoint += "?" + gemini_endpoint.split("?", 1)[1]
|
||
return async_endpoint
|
||
|
||
def build_submit_body(
|
||
self,
|
||
prompt: str,
|
||
model: str,
|
||
resolution: str,
|
||
aspect_ratio: str,
|
||
images: Optional[List[Image.Image]] = None,
|
||
**kwargs
|
||
) -> dict:
|
||
return self._client.build_request_body(
|
||
prompt=prompt,
|
||
images=images,
|
||
aspect_ratio=aspect_ratio,
|
||
resolution=resolution,
|
||
enable_grounding=kwargs.get("enable_grounding", False),
|
||
enable_image_search=kwargs.get("enable_image_search", False),
|
||
)
|
||
|
||
def extract_task_id(self, response: dict) -> str:
|
||
task_id = response.get("task_id")
|
||
if not task_id:
|
||
raise RuntimeError(f"提交响应中未找到 task_id: {response}")
|
||
return task_id
|
||
|
||
def extract_status(self, response: dict) -> str:
|
||
return response.get("status", "UNKNOWN")
|
||
|
||
async def parse_result(self, result_data: dict, session) -> List[Image.Image]:
|
||
# 异步接口可能直接返回 image_url
|
||
image_url = result_data.get("image_url", "") if isinstance(result_data, dict) else ""
|
||
if image_url:
|
||
async with session.get(image_url) as img_resp:
|
||
if img_resp.status == 200:
|
||
img_bytes = await img_resp.read()
|
||
return [Image.open(BytesIO(img_bytes))]
|
||
raise RuntimeError(f"下载图片失败 ({img_resp.status}): {image_url}")
|
||
|
||
# 否则按 Gemini 标准格式解析
|
||
images_list, _ = await self._client.parse_response_async(result_data, session=session)
|
||
return images_list
|
||
|
||
def get_models(self) -> List[str]:
|
||
return get_enabled_models()
|
||
|
||
def get_model_aspect_ratios(self, model_id: str) -> List[str]:
|
||
return get_model_supported_aspect_ratios(model_id)
|
||
|
||
def get_model_resolutions(self, model_id: str) -> List[str]:
|
||
return get_model_supported_resolutions(model_id)
|
||
|
||
# ========================================================================
|
||
# 可选方法覆盖
|
||
# ========================================================================
|
||
|
||
def get_extra_inputs(self) -> dict:
|
||
"""Gemini 专有:Google Search Grounding"""
|
||
return {
|
||
"联网功能": (["关闭", "打开"], {"default": "关闭"}),
|
||
}
|
||
|
||
def get_extra_kwargs(self, **kwargs) -> dict:
|
||
return {
|
||
"enable_grounding": kwargs.pop("联网功能", "关闭") == "打开",
|
||
}
|
||
|
||
def extract_progress(self, response: dict) -> Optional[float]:
|
||
"""从轮询响应中提取进度(0.0-1.0)"""
|
||
# 直接字段:progress / percentage
|
||
for field in ("progress", "percentage"):
|
||
val = response.get(field)
|
||
if val is not None and isinstance(val, (int, float)):
|
||
return val / 100.0 if val > 1 else float(val)
|
||
|
||
# 嵌套字段:progressInfo / progress_info
|
||
progress_info = response.get("progressInfo") or response.get("progress_info")
|
||
if isinstance(progress_info, dict):
|
||
for field in ("progress", "percentage"):
|
||
val = progress_info.get(field)
|
||
if val is not None and isinstance(val, (int, float)):
|
||
return val / 100.0 if val > 1 else float(val)
|
||
|
||
return None
|
||
|
||
def query_balance_sync(self) -> Optional[dict]:
|
||
try:
|
||
return self._client.query_balance_sync()
|
||
except Exception:
|
||
return None
|
||
|
||
def format_balance_info(self, balance_data: dict) -> str:
|
||
return self._client.format_balance_info(balance_data)
|