Simplify and standardize Chat Image Bridge 2 UI
This commit is contained in:
@@ -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
@@ -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
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user