diff --git a/README.md b/README.md index 2ec5985..ca96d57 100644 --- a/README.md +++ b/README.md @@ -74,31 +74,31 @@ api -> Chat Image Bridge -> Chat Image Bridge 可以直接接到 `Save Image` 或其他 ComfyUI 图像节点。 -## Gemini 3.1 简洁图片节点 +## Chat Image Bridge 2 添加节点: ```text -api -> Chat Image Bridge -> Gemini 3.1 图片生成 / 编辑 +api -> Chat Image Bridge -> Chat Image Bridge 2 ``` 该节点针对 `catapi.catcard.uk` 的 Gemini 原生 `generateContent` 接口。默认地址已经填写,日常使用时前面板只需要操作提示词、输出比例和分辨率。 ### 简洁界面 -- 左侧图片输入全部平等命名为“图片1、图片2……”;不预设主图和参考图。 -- 初始只显示图片1。连接最后一个图片口后自动增加下一个,最多14张;断开末尾多余连接后自动收起。 -- 用户可以在提示词中写“以图片1的人物、图片2的服装为参考”,插件会自动给图片加顺序标签。 +- 左侧图片输入全部平等命名为 `Image 1`、`Image 2`……;不预设主图和参考图。 +- 初始只显示 `Image 1`。连接最后一个图片口后自动增加下一个,最多14张;断开末尾多余连接后自动收起。 +- 用户可以在提示词中写“use the person from Image 1 and the clothes from Image 2”,插件会自动给图片加英文顺序标签。 - 没有连接图片时自动进行文生图,连接图片后自动进行编辑或参考图生成。 - 右侧只有一个 IMAGE 输出。 -- Base URL、API Key 和超时设置默认折叠在“连接设置”中。 +- Base URL 和 API Key 默认折叠在 `Connection Settings` 中。请求超时使用内部安全默认值,不占用前面板空间。 前面板核心参数: ```text -提示词 -输出比例 -分辨率 +Prompt +Output Ratio +Resolution ``` 分辨率会同时切换模型名和发送 `imageConfig.imageSize`: @@ -111,19 +111,19 @@ api -> Chat Image Bridge -> Gemini 3.1 图片生成 / 编辑 输出比例使用一个下拉框: -- `自动 / 保持原图`:不发送 `aspectRatio`。单图编辑保持输入比例,多图输入由模型结合提示词决定。 -- `方形 · 1:1`:方形输出。 -- `横图 · 16:9`、`横图 · 21:9` 等:横向输出。 -- `竖图 · 9:16`、`竖图 · 3:4` 等:竖向输出。 -- `自定义比例`:此时才显示自定义输入框,例如填写 `20:9`,节点会自动映射到最接近的服务端合法比例。 +- `Auto / Keep Input`:不发送 `aspectRatio`。单图编辑保持输入比例,多图输入由模型结合提示词决定。 +- `Square · 1:1`:方形输出。 +- `Landscape · 16:9`、`Landscape · 21:9` 等:横向输出。 +- `Portrait · 9:16`、`Portrait · 3:4` 等:竖向输出。 +- `Custom Ratio`:此时才显示自定义输入框,例如填写 `20:9`,节点会自动映射到最接近的服务端合法比例。 -比例下方带有一个只读画幅预览,不增加额外参数。 +节点不再绘制额外的比例示意图,完全使用原生 ComfyUI 控件,避免界面覆盖问题。 服务端当前支持:`1:1`、`1:4`、`1:8`、`2:3`、`3:2`、`3:4`、`4:1`、`4:3`、`4:5`、`5:4`、`8:1`、`9:16`、`16:9`、`21:9`。服务端不直接接受 `20:9`,节点会将其映射为 `21:9`。 最多支持14个动态 ComfyUI `IMAGE` 输入。中转站实测可以接受20张,但模型的可靠公开能力为14张,因此节点将上限设置为14。 -实测任意比例的 `1000×437` 输入图选择“自动 / 保持原图 + 4K”后输出为 `6152×2688`,基本精确保留原图比例;同一输入选择“横图 · 16:9 + 4K”时输出为 `5504×3072`。因此节点既支持保持原画幅的高清重绘,也支持改变画幅与提升分辨率同时进行。 +实测任意比例的 `1000×437` 输入图选择 `Auto / Keep Input + 4K` 后输出为 `6152×2688`,基本精确保留原图比例;同一输入选择 `Landscape · 16:9 + 4K` 时输出为 `5504×3072`。因此节点既支持保持原画幅的高清重绘,也支持改变画幅与提升分辨率同时进行。 ## GPT Image 生图节点 diff --git a/chat_image_bridge.py b/chat_image_bridge.py index 93b5e99..7c6e885 100644 --- a/chat_image_bridge.py +++ b/chat_image_bridge.py @@ -47,22 +47,22 @@ CATAPI_GEMINI_ASPECT_RATIOS = [ "21:9", ] CATAPI_OUTPUT_RATIO_OPTIONS = [ - "自动 / 保持原图", - "方形 · 1:1", - "横图 · 16:9", - "横图 · 21:9", - "横图 · 4:3", - "横图 · 3:2", - "横图 · 5:4", - "横图 · 4:1", - "横图 · 8:1", - "竖图 · 9:16", - "竖图 · 3:4", - "竖图 · 2:3", - "竖图 · 4:5", - "竖图 · 1:4", - "竖图 · 1:8", - "自定义比例", + "Auto / Keep Input", + "Square · 1:1", + "Landscape · 16:9", + "Landscape · 21:9", + "Landscape · 4:3", + "Landscape · 3:2", + "Landscape · 5:4", + "Landscape · 4:1", + "Landscape · 8:1", + "Portrait · 9:16", + "Portrait · 3:4", + "Portrait · 2:3", + "Portrait · 4:5", + "Portrait · 1:4", + "Portrait · 1:8", + "Custom Ratio", ] GPT_IMAGE_DEFAULT_BASE_URL = "" GPT_IMAGE_MODELS = ["gpt-image-2-vip", "gpt-image-2", "nano-banana", "nano-banana-2", "nano-banana-pro"] @@ -239,9 +239,9 @@ def nearest_catapi_aspect_ratio(value): def resolve_catapi_output_ratio(output_ratio, custom_ratio=""): output_ratio = (output_ratio or "").strip() - if not output_ratio or output_ratio == "自动 / 保持原图": + if not output_ratio or output_ratio in ("Auto / Keep Input", "自动 / 保持原图"): return "", "auto" - if output_ratio == "自定义比例": + if output_ratio in ("Custom Ratio", "自定义比例"): requested = (custom_ratio or "").strip() return nearest_catapi_aspect_ratio(requested), requested @@ -851,12 +851,11 @@ class CatAPIGeminiImage(ChatImageBridgeBase): return { "required": { "prompt": ("STRING", {"default": "", "multiline": True}), - "output_ratio": (CATAPI_OUTPUT_RATIO_OPTIONS, {"default": "自动 / 保持原图"}), + "output_ratio": (CATAPI_OUTPUT_RATIO_OPTIONS, {"default": "Auto / Keep Input"}), "custom_ratio": ("STRING", {"default": "20:9", "multiline": False}), "resolution": (list(CATAPI_GEMINI_MODELS), {"default": "1K"}), "api_key": ("STRING", {"default": "", "multiline": False, "password": True}), "base_url": ("STRING", {"default": CATAPI_GEMINI_BASE_URL, "multiline": False}), - "timeout_seconds": ("INT", {"default": 600, "min": 60, "max": 3600}), }, "optional": { "image_1": ("IMAGE",), @@ -876,7 +875,6 @@ class CatAPIGeminiImage(ChatImageBridgeBase): resolution, api_key, base_url, - timeout_seconds, **kwargs, ): if not (api_key or "").strip(): @@ -905,15 +903,15 @@ class CatAPIGeminiImage(ChatImageBridgeBase): parts = [ { "text": ( - "以下图片按图片1、图片2的顺序编号。它们地位平等," - "请严格根据用户提示理解每张图片的用途。" + "The input images are labeled Image 1, Image 2, and so on. " + "They have equal status; infer each image's role only from the user's prompt." ) } ] for image_index, image in image_inputs: - parts.append({"text": f"图片{image_index}:"}) + parts.append({"text": f"Image {image_index}:"}) parts.append({"inlineData": tensor_to_png_inline_data(image)}) - parts.append({"text": f"用户要求:\n{prompt.strip()}"}) + parts.append({"text": f"User request:\n{prompt.strip()}"}) else: parts = [{"text": prompt.strip()}] @@ -932,7 +930,7 @@ class CatAPIGeminiImage(ChatImageBridgeBase): endpoint=endpoint, api_key=api_key, payload=payload, - timeout_seconds=int(timeout_seconds), + timeout_seconds=600, retry_times=1, ) refs = collect_image_references(data) @@ -940,7 +938,7 @@ class CatAPIGeminiImage(ChatImageBridgeBase): preview = json.dumps(redact_object(data), ensure_ascii=False)[:2000] raise RuntimeError(f"Gemini response did not contain image data: {preview}") - output_image, _ = self._decode_refs_to_image(refs, int(timeout_seconds)) + output_image, _ = self._decode_refs_to_image(refs, 600) height = int(output_image.shape[1]) width = int(output_image.shape[2]) info = { @@ -1186,7 +1184,7 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "ChatImageBridge": "Chat Image Bridge", "ChatImageBridgeAdvanced": "Chat Image Bridge Advanced", - "CatAPIGeminiImage": "Gemini 3.1 图片生成 / 编辑", + "CatAPIGeminiImage": "Chat Image Bridge 2", "GPTImage": "GPT Image Generate", "GPTImageEdit": "GPT Image Edit", "RightCodesGPTImage": "GPT Image", diff --git a/web/chat_image_bridge.js b/web/chat_image_bridge.js index bd65a5d..82ca674 100644 --- a/web/chat_image_bridge.js +++ b/web/chat_image_bridge.js @@ -5,8 +5,7 @@ const NODE_CLASS = "ChatImageBridge"; const FETCH_BUTTON = "Fetch Models"; const CATAPI_NODE_CLASS = "CatAPIGeminiImage"; const MAX_CATAPI_IMAGES = 14; -const CATAPI_AUTO_RATIO = "自动 / 保持原图"; -const CATAPI_CUSTOM_RATIO = "自定义比例"; +const CATAPI_CUSTOM_RATIO = "Custom Ratio"; function findWidget(node, name) { return node.widgets?.find((widget) => widget.name === name); @@ -133,16 +132,19 @@ function setWidgetVisible(node, widget, visible) { widget.type = widget.__catapiHidden.type; widget.computeSize = widget.__catapiHidden.computeSize; widget.serializeValue = widget.__catapiHidden.serializeValue; + widget.hidden = widget.__catapiHidden.hidden; delete widget.__catapiHidden; } else if (!visible && !widget.__catapiHidden) { widget.__catapiHidden = { type: widget.type, computeSize: widget.computeSize, serializeValue: widget.serializeValue, + hidden: widget.hidden, }; widget.type = "converted-widget"; widget.computeSize = () => [0, -4]; widget.serializeValue = () => widget.value; + widget.hidden = true; } const computed = node.computeSize?.() || node.size; @@ -150,67 +152,6 @@ function setWidgetVisible(node, widget, visible) { markCanvasDirty(); } -function parseRatioValue(value) { - const match = `${value || ""}`.match(/(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)\s*$/); - if (!match) { - return null; - } - const width = Number(match[1]); - const height = Number(match[2]); - return width > 0 && height > 0 ? width / height : null; -} - -function currentPreviewRatio(node) { - const outputRatio = `${findWidget(node, "output_ratio")?.value || ""}`; - if (outputRatio === CATAPI_AUTO_RATIO) { - return null; - } - if (outputRatio === CATAPI_CUSTOM_RATIO) { - return parseRatioValue(findWidget(node, "custom_ratio")?.value); - } - return parseRatioValue(outputRatio); -} - -function makeRatioPreviewWidget(node) { - return { - name: "ratio_preview", - type: "custom", - value: null, - serialize: false, - computeSize() { - return [360, 62]; - }, - draw(ctx, _node, width, y, height) { - const ratio = currentPreviewRatio(node); - const centerX = width / 2; - const centerY = y + height / 2 + 2; - ctx.save(); - ctx.lineWidth = 1.5; - ctx.strokeStyle = "#8a8a8a"; - ctx.fillStyle = "#b8b8b8"; - ctx.font = "12px sans-serif"; - ctx.textAlign = "left"; - ctx.fillText(ratio ? (ratio > 1 ? "横图" : ratio < 1 ? "竖图" : "方形") : "自动 / 保持原图", 14, centerY + 4); - - if (ratio) { - const maxWidth = 105; - const maxHeight = 42; - let boxWidth = maxWidth; - let boxHeight = boxWidth / ratio; - if (boxHeight > maxHeight) { - boxHeight = maxHeight; - boxWidth = boxHeight * ratio; - } - ctx.strokeRect(centerX + 24 - boxWidth / 2, centerY - boxHeight / 2, boxWidth, boxHeight); - } else { - ctx.setLineDash([5, 4]); - ctx.strokeRect(centerX - 28, centerY - 18, 80, 36); - } - ctx.restore(); - }, - }; -} - function catapiImageInputs(node) { return (node.inputs || []).filter((input) => /^image_\d+$/.test(input.name)); } @@ -218,7 +159,7 @@ function catapiImageInputs(node) { function labelCatapiImageInputs(node) { for (const input of catapiImageInputs(node)) { const index = Number(input.name.split("_")[1]); - input.label = `图片${index}`; + input.label = `Image ${index}`; } } @@ -250,17 +191,16 @@ function updateCatapiImageInputs(node) { } function prepareCatapiNode(node) { - node.title = "Gemini 3.1 图片生成 / 编辑"; + node.title = "Chat Image Bridge 2"; node.setSize?.([Math.max(node.size?.[0] || 380, 380), node.size?.[1] || 240]); const labels = { - prompt: "提示词", - output_ratio: "输出比例", - custom_ratio: "自定义比例", - resolution: "分辨率", + prompt: "Prompt", + output_ratio: "Output Ratio", + custom_ratio: "Custom Ratio", + resolution: "Resolution", api_key: "API Key", base_url: "Base URL", - timeout_seconds: "超时时间", }; for (const [name, label] of Object.entries(labels)) { const widget = findWidget(node, name); @@ -284,21 +224,7 @@ function prepareCatapiNode(node) { }; ratioWidget.__catapiCallbackWrapped = true; } - if (customRatioWidget && !customRatioWidget.__catapiCallbackWrapped) { - const originalCallback = customRatioWidget.callback; - customRatioWidget.callback = function () { - const result = originalCallback?.apply(this, arguments); - markCanvasDirty(); - return result; - }; - customRatioWidget.__catapiCallbackWrapped = true; - } - - if (node.addCustomWidget && !node.widgets?.some((widget) => widget.name === "ratio_preview")) { - node.addCustomWidget(makeRatioPreviewWidget(node)); - } - - const advancedWidgets = ["api_key", "base_url", "timeout_seconds"].map((name) => findWidget(node, name)); + const advancedWidgets = ["api_key", "base_url"].map((name) => findWidget(node, name)); node.properties = node.properties || {}; if (node.properties.catapiConnectionExpanded == null) { node.properties.catapiConnectionExpanded = false; @@ -310,7 +236,7 @@ function prepareCatapiNode(node) { setWidgetVisible(node, widget, expanded); } if (connectionButton) { - connectionButton.label = expanded ? "收起连接设置" : "⚙ 连接设置"; + connectionButton.label = expanded ? "Hide Connection Settings" : "Connection Settings"; connectionButton.name = "connection_settings"; } }; @@ -326,7 +252,7 @@ function prepareCatapiNode(node) { updateConnectionWidgets(); updateCatapiImageInputs(node); if (node.outputs?.[0]) { - node.outputs[0].label = "图像"; + node.outputs[0].label = "Image"; } }