From 0b36551043535e8cdd3780a99c8a0a5562a210ed Mon Sep 17 00:00:00 2001 From: rui40000 Date: Tue, 31 Mar 2026 09:41:31 +0800 Subject: [PATCH] Xmiles-nanobanana: add on_queued behavior (passthrough/empty, default passthrough) + generic Poller node for async completion --- xmiles_nanobanana_node.py | 94 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/xmiles_nanobanana_node.py b/xmiles_nanobanana_node.py index 8264036..4cfdc9c 100644 --- a/xmiles_nanobanana_node.py +++ b/xmiles_nanobanana_node.py @@ -27,6 +27,7 @@ class XmilesNanobananaNode: "proxy_url": ("STRING", {"default": "", "multiline": False}), "verbose": ("BOOLEAN", {"default": True}), "use_oss": ("BOOLEAN", {"default": False}), + "on_queued": (["passthrough", "empty"], {"default": "passthrough"}), "oss_access_key": ("STRING", {"default": "", "multiline": False}), "oss_secret_key": ("STRING", {"default": "", "multiline": False}), "oss_endpoint": ("STRING", {"default": "", "multiline": False, "placeholder": "oss-cn-shanghai.aliyuncs.com"}), @@ -122,7 +123,7 @@ class XmilesNanobananaNode: img = Image.open(io.BytesIO(r.content)) return self._pil_to_tensor(img) - def generate(self, text, resolution, aspect_ratio, images=None, proxy_url="", verbose=True, use_oss=False, + def generate(self, text, resolution, aspect_ratio, images=None, proxy_url="", verbose=True, use_oss=False, on_queued="passthrough", oss_access_key="", oss_secret_key="", oss_endpoint="", oss_bucket_name="", oss_url_prefix=""): t0 = time.perf_counter() logs = [] @@ -246,7 +247,7 @@ class XmilesNanobananaNode: logs.append(f"post_elapsed_ms={(t4-t3)*1000:.2f}") status = data.get("status") gen_status = data.get("generateStatus") - logs = {"status": status, "generateStatus": gen_status, "clientId": data.get("clientId"), "timestamp": data.get("timestamp")} + logs = {"status": status, "generateStatus": gen_status, "clientId": data.get("clientId"), "timestamp": data.get("timestamp"), "raw": data} tensors = [] if status == 0 and gen_status == 1: items = data.get("data") or [] @@ -267,7 +268,14 @@ class XmilesNanobananaNode: else: if verbose: print("Xmiles-nanobanana:task_failed", {"status": status, "generateStatus": gen_status}, flush=True) - return ([], json.dumps(data, ensure_ascii=False)) + if on_queued == "passthrough" and images is not None: + passthrough = [] + if isinstance(images, list): + passthrough = [img[0] for img in images] + else: + passthrough = [images[0]] + return (passthrough, json.dumps(logs, ensure_ascii=False)) + return ([], json.dumps(logs, ensure_ascii=False)) except Exception as e: if verbose: print("Xmiles-nanobanana:error", str(e), flush=True) @@ -335,12 +343,92 @@ class XmilesNanobananaResultParser: print("Xmiles-nanobanana:parse_error", str(e), flush=True) return ([], str(e)) +class XmilesNanobananaPoller: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "query_url": ("STRING", {"default": "", "multiline": False}), + "method": (["GET", "POST"], {"default": "GET"}), + "client_id": ("STRING", {"default": "", "multiline": False}), + "unique_id": ("STRING", {"default": "", "multiline": False}), + "poll_interval_ms": ("INT", {"default": 2000, "min": 200, "max": 60000, "step": 100}), + "max_wait_ms": ("INT", {"default": 60000, "min": 2000, "max": 600000, "step": 1000}), + }, + "optional": { + "payload_template": ("STRING", {"multiline": True, "default": ""}), + "proxy_url": ("STRING", {"default": "", "multiline": False}), + "verbose": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("images", "log") + OUTPUT_IS_LIST = (True, False) + FUNCTION = "poll" + CATEGORY = "Rui-Node🐶/AI模型🤖" + + def _download_image_tensor(self, url, proxies=None): + r = requests.get(url, proxies=proxies, timeout=60) + r.raise_for_status() + img = Image.open(io.BytesIO(r.content)) + if img.mode != "RGB": + img = img.convert("RGB") + np_img = np.array(img).astype(np.float32) / 255.0 + return torch.from_numpy(np_img).unsqueeze(0) + + def poll(self, query_url, method, client_id, unique_id, poll_interval_ms, max_wait_ms, payload_template="", proxy_url="", verbose=True): + t_start = time.perf_counter() + proxies = None + if proxy_url and proxy_url.strip(): + proxies = {"http": proxy_url, "https": proxy_url} + last_obj = None + while (time.perf_counter() - t_start) * 1000 < max_wait_ms: + try: + body = None + headers = {"Content-Type": "application/json"} + if payload_template and payload_template.strip(): + body_str = payload_template.replace("{clientId}", client_id).replace("{uniqueId}", unique_id) + try: + body = json.loads(body_str) + except: + body = body_str + if verbose: + print("Xmiles-nanobanana:poll_tick", {"url": query_url}, flush=True) + if method == "POST": + r = requests.post(query_url, headers=headers, json=body if isinstance(body, dict) else None, data=None if isinstance(body, dict) else body, proxies=proxies, timeout=30) + else: + params = {"clientId": client_id, "uniqueId": unique_id} + r = requests.get(query_url, headers=headers, params=params, proxies=proxies, timeout=30) + r.raise_for_status() + obj = r.json() + last_obj = obj + status = obj.get("status") + gen_status = obj.get("generateStatus") + if verbose: + print("Xmiles-nanobanana:poll_status", {"status": status, "generateStatus": gen_status}, flush=True) + if status == 0 and gen_status == 1: + tensors = [] + items = obj.get("data") or [] + for item in items: + url_item = item.get("url") + if url_item: + tensors.append(self._download_image_tensor(url_item, proxies=proxies)) + return (tensors, json.dumps(obj, ensure_ascii=False)) + except Exception as e: + if verbose: + print("Xmiles-nanobanana:poll_error", str(e), flush=True) + time.sleep(poll_interval_ms / 1000.0) + return ([], json.dumps(last_obj if last_obj is not None else {"error": "timeout"}, ensure_ascii=False)) + NODE_CLASS_MAPPINGS = { "XmilesNanobanana": XmilesNanobananaNode, "XmilesNanobananaResultParser": XmilesNanobananaResultParser, + "XmilesNanobananaPoller": XmilesNanobananaPoller, } NODE_DISPLAY_NAME_MAPPINGS = { "XmilesNanobanana": "Xmiles-nanobanana", "XmilesNanobananaResultParser": "Xmiles-nanobanana 结果解析", + "XmilesNanobananaPoller": "Xmiles-nanobanana 轮询查询", }