Simplify and standardize Chat Image Bridge 2 UI

This commit is contained in:
Marigold1122
2026-07-26 12:38:55 +08:00
parent 08e56a1248
commit ab2a9142e4
3 changed files with 55 additions and 131 deletions
+16 -16
View File
@@ -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 生图节点
+26 -28
View File
@@ -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",
+13 -87
View File
@@ -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";
}
}