diff --git a/__init__.py b/__init__.py index 0476524..2ab34de 100644 --- a/__init__.py +++ b/__init__.py @@ -30,6 +30,8 @@ from .color_matcher_node import NODE_DISPLAY_NAME_MAPPINGS as COLORMATCHER_NODE_ # 新增:素材拆分节点 from .image_splitter_node import NODE_CLASS_MAPPINGS as IMAGESPLITTER_NODE_CLASS_MAPPINGS from .image_splitter_node import NODE_DISPLAY_NAME_MAPPINGS as IMAGESPLITTER_NODE_DISPLAY_NAME_MAPPINGS +from .xmiles_nanobanana_node import NODE_CLASS_MAPPINGS as XMILES_NODE_CLASS_MAPPINGS +from .xmiles_nanobanana_node import NODE_DISPLAY_NAME_MAPPINGS as XMILES_NODE_DISPLAY_NAME_MAPPINGS # 合并节点映射字典 NODE_CLASS_MAPPINGS = {} @@ -48,6 +50,7 @@ NODE_CLASS_MAPPINGS.update(UTF8_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(OPENAI_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(COLORMATCHER_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(IMAGESPLITTER_NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(XMILES_NODE_CLASS_MAPPINGS) # 合并节点显示名称映射 NODE_DISPLAY_NAME_MAPPINGS = {} @@ -66,5 +69,6 @@ NODE_DISPLAY_NAME_MAPPINGS.update(UTF8_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(OPENAI_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(COLORMATCHER_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(IMAGESPLITTER_NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(XMILES_NODE_DISPLAY_NAME_MAPPINGS) -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..83e6415 Binary files /dev/null and b/__pycache__/__init__.cpython-310.pyc differ diff --git a/__pycache__/dialogue_extractor_node.cpython-310.pyc b/__pycache__/dialogue_extractor_node.cpython-310.pyc new file mode 100644 index 0000000..c7b8700 Binary files /dev/null and b/__pycache__/dialogue_extractor_node.cpython-310.pyc differ diff --git a/__pycache__/flip_node.cpython-310.pyc b/__pycache__/flip_node.cpython-310.pyc new file mode 100644 index 0000000..264fc7b Binary files /dev/null and b/__pycache__/flip_node.cpython-310.pyc differ diff --git a/__pycache__/load_image_node.cpython-310.pyc b/__pycache__/load_image_node.cpython-310.pyc new file mode 100644 index 0000000..1d8777c Binary files /dev/null and b/__pycache__/load_image_node.cpython-310.pyc differ diff --git a/__pycache__/mask_preview_node.cpython-310.pyc b/__pycache__/mask_preview_node.cpython-310.pyc new file mode 100644 index 0000000..5c243e9 Binary files /dev/null and b/__pycache__/mask_preview_node.cpython-310.pyc differ diff --git a/__pycache__/mask_selector_node.cpython-310.pyc b/__pycache__/mask_selector_node.cpython-310.pyc new file mode 100644 index 0000000..11c42cb Binary files /dev/null and b/__pycache__/mask_selector_node.cpython-310.pyc differ diff --git a/__pycache__/page_narration_remover_node.cpython-310.pyc b/__pycache__/page_narration_remover_node.cpython-310.pyc new file mode 100644 index 0000000..a765271 Binary files /dev/null and b/__pycache__/page_narration_remover_node.cpython-310.pyc differ diff --git a/__pycache__/qwenedit_node.cpython-310.pyc b/__pycache__/qwenedit_node.cpython-310.pyc new file mode 100644 index 0000000..fba6b6f Binary files /dev/null and b/__pycache__/qwenedit_node.cpython-310.pyc differ diff --git a/__pycache__/saturation_node.cpython-310.pyc b/__pycache__/saturation_node.cpython-310.pyc new file mode 100644 index 0000000..ec0d87a Binary files /dev/null and b/__pycache__/saturation_node.cpython-310.pyc differ diff --git a/__pycache__/shot_splitter_node.cpython-310.pyc b/__pycache__/shot_splitter_node.cpython-310.pyc new file mode 100644 index 0000000..4655f3c Binary files /dev/null and b/__pycache__/shot_splitter_node.cpython-310.pyc differ diff --git a/__pycache__/text_list_node.cpython-310.pyc b/__pycache__/text_list_node.cpython-310.pyc new file mode 100644 index 0000000..5cb9675 Binary files /dev/null and b/__pycache__/text_list_node.cpython-310.pyc differ diff --git a/xmiles_nanobanana_node.py b/xmiles_nanobanana_node.py new file mode 100644 index 0000000..374cac3 --- /dev/null +++ b/xmiles_nanobanana_node.py @@ -0,0 +1,198 @@ +import torch +import numpy as np +import requests +import json +import uuid +import io +import base64 +from PIL import Image + +class XmilesNanobananaNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"multiline": True, "default": ""}), + "resolution": (["1K", "2K", "4K"], {"default": "4K"}), + "aspect_ratio": (["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"], {"default": "9:16"}), + }, + "optional": { + "images": ("IMAGE",), + "proxy_url": ("STRING", {"default": "", "multiline": False}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("images", "log") + OUTPUT_IS_LIST = (True, False) + FUNCTION = "generate" + CATEGORY = "Rui-Node🐶/AI模型🤖" + + def _make_client_id(self): + return f"{uuid.uuid4()}-test" + + def _tensor_to_png_base64(self, tensor): + arr = tensor.cpu().numpy() + arr = np.clip(arr, 0, 1) + img = Image.fromarray((arr * 255).astype(np.uint8), 'RGB') + buf = io.BytesIO() + img.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("utf-8") + + def _pil_to_tensor(self, pil_img): + if pil_img.mode != "RGB": + pil_img = pil_img.convert("RGB") + np_img = np.array(pil_img).astype(np.float32) / 255.0 + t = torch.from_numpy(np_img).unsqueeze(0) + return t + + 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)) + return self._pil_to_tensor(img) + + def generate(self, text, resolution, aspect_ratio, images=None, proxy_url=""): + client_id = self._make_client_id() + proxies = None + if proxy_url and proxy_url.strip(): + proxies = {"http": proxy_url, "https": proxy_url} + + parts = [] + if text and text.strip(): + parts.append({"text": text}) + + image_parts = [] + if images is not None: + if isinstance(images, list): + tensors = [img[0] for img in images] + else: + tensors = [images[0]] + for t in tensors: + b64 = self._tensor_to_png_base64(t) + image_parts.append({"inlineData": {"data": "data:image/png;base64," + b64, "mimeType": "image/png"}}) + + for p in image_parts: + parts.append(p) + + body_obj = { + "contents": [ + { + "parts": parts, + "role": "user" + } + ], + "filePath": { + "inputs": { + "filePath": "" + } + }, + "generationConfig": { + "candidateCount": 1, + "imageConfig": { + "aspectRatio": aspect_ratio, + "imageSize": resolution + }, + "responseModalities": ["TEXT", "IMAGE"], + "temperature": 1.0, + "topP": 0.95 + }, + "model": "gemini-3.1-flash-image-preview" + } + + payload = { + "taskType": "ZENMUX", + "clientId": client_id, + "clientType": "image", + "callBackService": "remoteApi", + "extraData": { + "faceDetailer": 0, + "filePath": "", + "loraNum": 0, + "memberType": "PLUS", + "moduleName": "全能编辑 V2", + "resolution": "", + "taskType": "ZENMUX", + "uniqueId": str(uuid.uuid4().int)[:19], + "workflowName": "" + }, + "imgIdList": [], + "memberType": "PLUS", + "body": json.dumps(body_obj, ensure_ascii=False) + } + + url = "https://test.holopix.cn/ai-holopix-queue/api/prompt" + try: + resp = requests.post(url, headers={"Content-Type": "application/json"}, json=payload, proxies=proxies, timeout=60) + resp.raise_for_status() + data = resp.json() + status = data.get("status") + gen_status = data.get("generateStatus") + logs = {"status": status, "generateStatus": gen_status, "clientId": data.get("clientId"), "timestamp": data.get("timestamp")} + tensors = [] + if status == 0 and gen_status == 1: + items = data.get("data") or [] + for item in items: + url_item = item.get("url") + if url_item: + t = self._download_image_tensor(url_item, proxies=proxies) + tensors.append(t) + return (tensors if tensors else [], json.dumps(logs, ensure_ascii=False)) + else: + return ([], json.dumps(data, ensure_ascii=False)) + except Exception as e: + return ([], str(e)) + +class XmilesNanobananaResultParser: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "json_text": ("STRING", {"multiline": True, "default": ""}), + "proxy_url": ("STRING", {"default": "", "multiline": False}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("images", "log") + OUTPUT_IS_LIST = (True, False) + FUNCTION = "parse" + 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 parse(self, json_text, proxy_url=""): + proxies = None + if proxy_url and proxy_url.strip(): + proxies = {"http": proxy_url, "https": proxy_url} + try: + obj = json.loads(json_text) + status = obj.get("status") + gen_status = obj.get("generateStatus") + tensors = [] + if status == 0 and gen_status == 1: + 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({"status": status, "generateStatus": gen_status}, ensure_ascii=False)) + except Exception as e: + return ([], str(e)) + +NODE_CLASS_MAPPINGS = { + "XmilesNanobanana": XmilesNanobananaNode, + "XmilesNanobananaResultParser": XmilesNanobananaResultParser, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "XmilesNanobanana": "Xmiles-nanobanana", + "XmilesNanobananaResultParser": "Xmiles-nanobanana 结果解析", +}