diff --git a/clients/gemini_client.py b/clients/gemini_client.py index 43ef26a..3b085d9 100644 --- a/clients/gemini_client.py +++ b/clients/gemini_client.py @@ -115,13 +115,13 @@ class GeminiAPIClient(BaseAPIClient): return "/v1beta/models/gemini-3-pro-image-preview:generateContent" def get_http_error_message(self, status_code: int, error_message: str) -> Optional[str]: - """Gemini 请求 429/503 时返回图中约定的多行错误框文案。""" + """Gemini 请求 429/503/504 时返回中文错误文案。""" if status_code == 429: - return "该型号资源已耗尽,但只是暂时的,请稍后重试。" + return "此型号资源暂时耗尽,继续重试即可" if status_code == 503: return "此型号目前需求量较大。需求高峰通常是暂时的。请稍后再试。" if status_code == 504: - return "服务无法在截止期限内完成处理。请稍后重试。" + return "服务无法在截止期限内完成处理。可能原因是:您的提示词过大,无法及时处理。" return None def build_request_body( diff --git a/clients/gpt_image_client.py b/clients/gpt_image_client.py index 3b13d78..23fa308 100644 --- a/clients/gpt_image_client.py +++ b/clients/gpt_image_client.py @@ -417,3 +417,35 @@ class GptImageClient: raise RuntimeError( f"o1key GPT Image 请求超时(>{_REQUEST_TIMEOUT}s),请检查网络或稍后重试" ) + + # ── 余额查询 ────────────────────────────────────────────────────────────── + + async def _query_balance_async(self) -> dict: + url = f"{self.base_url}/api/usage/token" + connector = aiohttp.TCPConnector(ssl=False, force_close=True) + timeout = aiohttp.ClientTimeout(total=10) + async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session: + async with session.get(url, headers=self._auth_headers()) as resp: + if resp.status != 200: + raise RuntimeError(f"余额查询失败 HTTP {resp.status}") + return await resp.json() + + def query_balance_sync(self) -> dict: + def _run(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(self._query_balance_async()) + finally: + loop.close() + + with ThreadPoolExecutor(max_workers=1) as executor: + return executor.submit(_run).result(timeout=15) + + @staticmethod + def format_balance_info(balance_data: dict) -> str: + data = balance_data.get("data", {}) + api_name = data.get("name", "未知") + total_available = data.get("total_available", 0) + balance_in_dollars = total_available / 500000 + return f"当前余额:{balance_in_dollars:.2f} | API:{api_name}" diff --git a/nodes/gpt_image.py b/nodes/gpt_image.py index 647d852..c44b88f 100644 --- a/nodes/gpt_image.py +++ b/nodes/gpt_image.py @@ -141,20 +141,44 @@ class O1keyGPTImage: raise ValueError("未授权!") from None raise - # ── 4. 解析批量提示词 ───────────────────────────────────────────────── - batch_prompts = parse_batch_prompts(prompt) + try: + # ── 4. 解析批量提示词 ───────────────────────────────────────────── + batch_prompts = parse_batch_prompts(prompt) - # ── 5. 调用 API ─────────────────────────────────────────────────────── - all_pil_images = [] + # ── 5. 调用 API ─────────────────────────────────────────────────── + all_pil_images = [] - if batch_prompts: - # 批量模式:逐条提示词调用 - total = len(batch_prompts) - print(f"[o1key GPT Image] 批量模式 | {total} 条提示词 | 每条生成 {生图数量} 张") - for idx, p in enumerate(batch_prompts, 1): + if batch_prompts: + # 批量模式:逐条提示词调用 + total = len(batch_prompts) + print(f"[o1key GPT Image] 批量模式 | {total} 条提示词 | 每条生成 {生图数量} 张") + for idx, p in enumerate(batch_prompts, 1): + try: + pil_images = client.run_sync( + prompt=p, + model=模型, + quality=quality, + background="auto", + size=size, + n=生图数量, + seed=seed, + image_tensor=图片, + mask_tensor=遮罩, + ) + all_pil_images.extend(pil_images) + snippet = p[:30] + ("..." if len(p) >= 30 else "") + print(f"[o1key GPT Image] [{idx}/{total}] ✓ {snippet}") + except Exception as e: + error_msg = str(e).split('\n')[0] + snippet = p[:30] + ("..." if len(p) >= 30 else "") + print(f"[o1key GPT Image] [{idx}/{total}] ❌ {snippet} → {error_msg}") + else: + # 单提示词模式 + if not prompt or not prompt.strip(): + raise ValueError("提示词不能为空") try: pil_images = client.run_sync( - prompt=p, + prompt=prompt, model=模型, quality=quality, background="auto", @@ -165,47 +189,35 @@ class O1keyGPTImage: mask_tensor=遮罩, ) all_pil_images.extend(pil_images) - snippet = p[:30] + ("..." if len(p) >= 30 else "") - print(f"[o1key GPT Image] [{idx}/{total}] ✓ {snippet}") except Exception as e: error_msg = str(e).split('\n')[0] - snippet = p[:30] + ("..." if len(p) >= 30 else "") - print(f"[o1key GPT Image] [{idx}/{total}] ❌ {snippet} → {error_msg}") - else: - # 单提示词模式 - if not prompt or not prompt.strip(): - raise ValueError("提示词不能为空") - try: - pil_images = client.run_sync( - prompt=prompt, - model=模型, - quality=quality, - background="auto", - size=size, - n=生图数量, - seed=seed, - image_tensor=图片, - mask_tensor=遮罩, - ) - all_pil_images.extend(pil_images) - except Exception as e: - error_msg = str(e).split('\n')[0] - print(f"[o1key GPT Image] ❌ {error_msg}") - raise RuntimeError(error_msg) from None + print(f"[o1key GPT Image] ❌ {error_msg}") + raise RuntimeError(error_msg) from None - # ── 6. 检查是否有可用图像 ───────────────────────────────────────────── - if not all_pil_images: - raise RuntimeError("所有提示词均生成失败,无可用图像输出") + # ── 6. 检查是否有可用图像 ───────────────────────────────────────── + if not all_pil_images: + raise RuntimeError("所有提示词均生成失败,无可用图像输出") - # ── 7. PIL → tensor ─────────────────────────────────────────────────── - output_tensor = GptImageClient._pil_list_to_tensor(all_pil_images) + # ── 7. PIL → tensor ─────────────────────────────────────────────── + output_tensor = GptImageClient._pil_list_to_tensor(all_pil_images) - # ── 8. 完成日志 ─────────────────────────────────────────────────────── - elapsed = time.time() - start_time - print( - f"[o1key GPT Image] 完成!耗时 {elapsed:.1f}s," - f"输出 {output_tensor.shape[0]} 张 " - f"{output_tensor.shape[2]}×{output_tensor.shape[1]}" - ) + # ── 8. 完成日志 ─────────────────────────────────────────────────── + elapsed = time.time() - start_time + print( + f"[o1key GPT Image] 完成!耗时 {elapsed:.1f}s," + f"输出 {output_tensor.shape[0]} 张 " + f"{output_tensor.shape[2]}×{output_tensor.shape[1]}" + ) - return (output_tensor,) + return (output_tensor,) + + finally: + self._print_balance(client) + + def _print_balance(self, client): + try: + balance_data = client.query_balance_sync() + balance_info = client.format_balance_info(balance_data) + print(f"[o1key GPT Image] {balance_info}") + except Exception: + pass diff --git a/nodes/nano_banana_pro.py b/nodes/nano_banana_pro.py index 495e29d..a6b3196 100644 --- a/nodes/nano_banana_pro.py +++ b/nodes/nano_banana_pro.py @@ -52,7 +52,7 @@ except ImportError: # ============================================================================ # 是否启用调试日志(打印完整的 API 响应内容) # 设置为 True 以启用调试日志,False 以禁用 -DEBUG_LOG_ENABLED = True +DEBUG_LOG_ENABLED = False # 是否启用请求体日志(打印发送给 API 的请求体,base64 图片数据将自动截断) # 设置为 True 以启用请求体日志,False 以禁用 REQUEST_LOG_ENABLED = False @@ -665,20 +665,37 @@ class NanoBananaPro: else: # 单提示词模式 if 生图数量 == 1: - # 单张:同步生成,输出 tensor - generated_images = self.client.generate_sync( - prompt=prompt, - model=模型, - resolution=分辨率, - aspect_ratio=宽高比, - batch_size=1, - images=input_images, - progress_callback=progress_callback, - debug=DEBUG_LOG_ENABLED, - debug_request=REQUEST_LOG_ENABLED, - enable_grounding=enable_grounding, - enable_image_search=enable_image_search, - ) + # 单张:同步生成,自动重试(429/503/504) + _RETRY_CODES = ("429", "503", "504") + _MAX_RETRIES = 5 + for _attempt in range(1, _MAX_RETRIES + 1): + try: + generated_images = self.client.generate_sync( + prompt=prompt, + model=模型, + resolution=分辨率, + aspect_ratio=宽高比, + batch_size=1, + images=input_images, + progress_callback=progress_callback, + debug=DEBUG_LOG_ENABLED, + debug_request=REQUEST_LOG_ENABLED, + enable_grounding=enable_grounding, + enable_image_search=enable_image_search, + ) + break + except RuntimeError as e: + error_msg = str(e) + if any(code in error_msg for code in _RETRY_CODES) and _attempt < _MAX_RETRIES: + _wait = 2 ** _attempt + print(f"{'=' * 60}") + print(f"⚠️ Nano Banana Pro 自动重试 [{_attempt}/{_MAX_RETRIES - 1}]") + print(f" 原因:{error_msg}") + print(f" 等待 {_wait}s 后重试...") + print(f"{'=' * 60}") + time.sleep(_wait) + else: + raise else: # 多张:异步并发,内存输出