diff --git a/CHANGELOG.md b/CHANGELOG.md index 751bb32..6984038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.9.0] - 2025-02-24 +### Added +- 增加腾讯AI翻译节点 + Add Tencent AI translation nod + e +- 增加Set&Get无线传输节点 + Add Set&Get wireless transmission node + ## [0.7.0] - 2025-01-19 ### Added diff --git a/README.md b/README.md index 297460c..a575c38 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,30 @@ Include predefined ID groups and group switches for persistent usage. Easy-to-edit and import presets for professional prompt enhancement. **Edit Path**: `KayTool/json/strong_prompt.json` +--- + +### 无线传输节点 Set&Get Node +- 无线发送和接收节点,用于保持工作流的整洁 +- 支持任意数据类型的传递 +- Wireless sending and receiving nodes to keep workflows clean +- Supports arbitrary data type transfer + +--- + +### 腾讯AI翻译 Tencent Translator Node + +- 无需的 API 密钥 +- 支持多种语言翻译 +- 自动检测输入语言 +- 快速启用或禁用翻译 +- 支持多种翻译场景 +- 国内可用的高质量 AI 翻译 +- No need for API keys +- Supports multiple languages for translation +- Automatically detects input language +- Quick enable/disable of translation +- Supports various translation scenarios +- High-quality AI translation available in China. --- ### 百度AI翻译 Baidu Translator Node @@ -37,13 +61,12 @@ Easy-to-edit and import presets for professional prompt enhancement. - 配置百度 API [链接](https://fanyi-api.baidu.com/) - 自动存储 API 信息避免重复输入 - 国内可用的高质量 AI 翻译 - -Support translation of two separate texts. -Support auto-detection, Simplified/Traditional Chinese, and English. -Quick enable/disable of translation. -Configure Baidu API [Link](https://fanyi-api.baidu.com/) -Automatically save API credentials to avoid repeated input. -High-quality AI translation accessible in China. +- Two texts (Text_A and Text_B) are supported for translation +- Support automatic detection, simplified/traditional Chinese and English +- Quickly enable or disable translation +- Configure Baidu API [link](https://fanyi-api.baidu.com/) +- Automatically store API information to avoid duplicate input +- High quality AI translation available in China --- @@ -54,6 +77,11 @@ High-quality AI translation accessible in China. - 返回整形和浮点型的结果,适用于需要数值结果的场景 - 提供算术、比较运算功能,支持自定义函数调用 +- Supports multiple data types as input (e.g. integers, floating-point numbers, strings representing numeric values, etc.) +- Use variables `a`, `b`, `c` to evaluate mathematical expressions +- Returns integer and floating-point results for scenarios where numerical results are required +- Arithmetic and comparison functions, custom function calls + #### 支持的表达式和基本用法 Supported Expressions and Basic Usage: ##### 算术运算 Arithmetic Operations: @@ -81,8 +109,6 @@ High-quality AI translation accessible in China. - 最小值 Min: `min(a, b)` - 最大值 Max: `max(a, b)` - 四舍五入 Round: `round(a)` -- 求和 Sum: `sum([a, b, c])` (接受列表参数) -- 长度 Len: `len([a, b, c])` (接受列表参数) ##### 示例用法 Example Usage: - 计算两数之和 Calculate sum: `value = "a + b"` @@ -109,9 +135,10 @@ Allow complex calculations by entering mathematical expressions in string form. - 返回字符串类型的输出,可用于后续处理 - 适合调试或检查工作流输出内容 -Accept any data type input and display converted text for inspection. -Return a string output for subsequent processing. -Ideal for debugging or checking workflow outputs. +- Accepts input of any data type (e.g. numbers, text, lists, etc.) +- Display converted text for easy viewing +- Returns output of string type that can be used for subsequent processing +- Suitable for debugging or checking workflow output --- @@ -122,8 +149,11 @@ Ideal for debugging or checking workflow outputs. - 色温:范围 -100 到 +100,负值增加蓝色,正值增加黄色 - 色调:范围 -100 到 +100,负值增加绿色,正值增加洋红 - 饱和度:范围 -100 到 +100 - -Adjust exposure, contrast, color temperature, hue, and saturation of images within specified ranges. + - Adjust exposure, contrast, color temperature, hue, and saturation of a picture + - Exposure and contrast: range-100 to +100 + - Color temperature: range-100 to +100, negative values increase blue, positive values increase yellow + - Hue: Range-100 to +100, negative values increase green, positive values increase magenta + - Saturation: Range-100 to +100 --- diff --git a/__init__.py b/__init__.py index db74b80..0a6c510 100644 --- a/__init__.py +++ b/__init__.py @@ -1,38 +1,34 @@ import os -# 导入所有节点类 from .nodes.custom_save_image import CustomSaveImage from .nodes.color_adjustment import ColorAdjustment from .nodes.strong_prompt import StrongPrompt from .nodes.baidu_translater import BaiduTranslater -from .nodes.display_any import DisplayAny # 添加新的 DisplayAny 节点 -from .nodes.abc_math import AbcMath # 确保路径正确 -from .nodes.image_size_extractor import ImageSizeExtractor # 添加 ImageSizeExtractor 节点 -from .nodes.tencent_translater import TencentTranslater # 导入 TencentTranslater 节点 +from .nodes.display_any import DisplayAny +from .nodes.abc_math import AbcMath +from .nodes.image_size_extractor import ImageSizeExtractor +from .nodes.tencent_translater import TencentTranslater -# 节点类映射 NODE_CLASS_MAPPINGS = { "Custom_Save_Image": CustomSaveImage, "Color_Adjustment": ColorAdjustment, "Strong_Prompt": StrongPrompt, - "Baidu_Translater": BaiduTranslater, + "Baidu_Translater": BaiduTranslater, "Display_Any": DisplayAny, "Abc_Math": AbcMath, - "Image_Size_Extractor": ImageSizeExtractor, # 添加 ImageSizeExtractor 映射 - "Tencent_Translater": TencentTranslater, # 添加 TencentTranslater 映射 + "Image_Size_Extractor": ImageSizeExtractor, + "Tencent_Translater": TencentTranslater, } -# 节点显示名称映射 NODE_DISPLAY_NAME_MAPPINGS = { "Custom_Save_Image": "Custom Save Image", "Color_Adjustment": "Color Adjustment", "Strong_Prompt": "Strong Prompt", - "Baidu_Translater": "Baidu Translater", + "Baidu_Translater": "Baidu Translater", "Display_Any": "Display Any", - "Abc_Math": "abc Math", # 显示名称映射 - "Image_Size_Extractor": "Image Size Extractor", # 添加 Image Size Extractor 显示名称映射 - "Tencent_Translater": "Tencent Translater", # 添加 Tencent Translater 显示名称映射 + "Abc_Math": "abc Math", + "Image_Size_Extractor": "Image Size Extractor", + "Tencent_Translater": "Tencent Translater", } -# 定义 Web 目录路径 WEB_DIRECTORY = "web" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c63ad0f..5415084 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "kaytool" -description ="This is a versatile and ever-expanding toolkit for ComfyUI, offering powerful node functionalities such as “Custom Save Image,” “Professional Color Adjustment,” and “Prompt Enhancer.” Its features include precise image saving with support for ICC color profiles (sRGB/Adobe RGB), metadata embedding, advanced image adjustments (exposure, contrast, color temperature, hue, saturation), professional filter previews, dynamic prompt editing, and high-quality Baidu AI translation." -version = "0.8.0" +description ="This is a versatile and ever-expanding toolkit for ComfyUI, offering powerful node functionalities such as “Custom Save Image,” “Professional Color Adjustment,” and “Prompt Enhancer.” Its features include precise image saving with support for ICC color profiles (sRGB/Adobe RGB), metadata embedding, advanced image adjustments (exposure, contrast, color temperature, hue, saturation), professional filter previews, dynamic prompt editing, and high-quality Baidu and Tencent AI translation." +version = "0.9.0" license = {file = "LICENSE"} [project.urls] diff --git a/web/appearance.js b/web/appearance.js index b4f1035..e95fcbe 100644 --- a/web/appearance.js +++ b/web/appearance.js @@ -14,7 +14,6 @@ const COLOR_THEMES = { none: { nodeColor: null, nodeBgColor: null } }; - const NODE_COLORS = { "Strong_Prompt": "blue", "Baidu_Translater": "green", @@ -24,23 +23,22 @@ const NODE_COLORS = { "Custom_Save_Image": "black", "Abc_Math": "cyan", "Image_Size_Extractor": "pale_blue" - }; function setNodeColors(node, theme) { if (!theme) return; - node.shape = "box"; + node.shape = "box"; if (theme.nodeColor && theme.nodeBgColor) { node.color = theme.nodeColor; node.bgcolor = theme.nodeBgColor; } } -// 创建扩展对象并注册 + app.registerExtension({ name: "kaytool.appearance", nodeCreated(node) { - // 检查是否需要应用主题 + if (NODE_COLORS.hasOwnProperty(node.comfyClass)) { const colorKey = NODE_COLORS[node.comfyClass]; const theme = COLOR_THEMES[colorKey]; diff --git a/web/kaynodes.js b/web/kaynodes.js new file mode 100644 index 0000000..ead0473 --- /dev/null +++ b/web/kaynodes.js @@ -0,0 +1,18 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "KayTool.KayNodes", + async setup() { + console.log("[KayTool] Enhancing virtual connections"); + const originalComputeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes; + LGraphCanvas.prototype.computeVisibleNodes = function () { + const visibleNodesSet = new Set(originalComputeVisibleNodes.apply(this, arguments)); + for (const node of this.graph._nodes) { + if ((node.type === "KaySetNode" || node.type === "KayGetNode") && node.drawConnection) { + visibleNodesSet.add(node); + } + } + return Array.from(visibleNodesSet); + }; + } +}); \ No newline at end of file diff --git a/web/setget.js b/web/setget.js new file mode 100644 index 0000000..9d71456 --- /dev/null +++ b/web/setget.js @@ -0,0 +1,384 @@ +import { app } from "../../../scripts/app.js"; + +//基于diffus 3的SetGet:https://github.com/diffus3/ComfyUI-extensions + +console.log("[KayTool] Loading SetGet extension"); + +const LGraphNode = LiteGraph.LGraphNode; + +function showAlert(message) { + app.extensionManager.toast.add({ + severity: 'warn', + summary: "KayTool Set/Get", + detail: `${message}. Most likely you're missing custom nodes`, + life: 5000, + }); +} + +app.registerExtension({ + name: "KayTool.SetGet", + registerCustomNodes() { + console.log("[KayTool] Registering KaySetNode and KayGetNode"); + + class KaySetNode extends LGraphNode { + defaultVisibility = true; + serialize_widgets = true; + drawConnection = false; + currentGetters = null; + slotColor = "#FFF"; + canvas = app.canvas; + menuEntry = "Show connections"; + + constructor(title) { + super(title); + + // 直接设置节点样式 + this.shape = "box"; // 设置节点形状 + this.color = "#222"; // 设置节点颜色 + this.bgcolor = "#000"; // 设置节点背景颜色 + + if (!this.properties) { + this.properties = { "previousName": "" }; + } + this.properties.showOutputText = KaySetNode.defaultVisibility; + + const node = this; + + this.addWidget( + "text", + "ID", + '', + (s) => { + node.validateName(node.graph); + if (this.widgets[0].value !== '') { + this.title = "🛜Set_" + this.widgets[0].value; + } + this.update(); + this.properties.previousName = this.widgets[0].value; + }, + {} + ); + + this.addInput("*", "*"); + this.addOutput("*", '*'); + + this.onConnectionsChange = function (slotType, slot, isChangeConnect, link_info) { + if (slotType === 1 && !isChangeConnect) { // Disconnect + if (this.inputs[slot].name === '') { + this.inputs[slot].type = '*'; + this.inputs[slot].name = '*'; + this.title = "🛜Set"; + this.widgets[0].value = ''; + } + } + if (slotType === 2 && !isChangeConnect) { + this.outputs[slot].type = '*'; + this.outputs[slot].name = '*'; + } + if (link_info && node.graph && slotType === 1 && isChangeConnect) { // Connect + const fromNode = node.graph._nodes.find((n) => n.id === link_info.origin_id); + if (fromNode && fromNode.outputs && fromNode.outputs[link_info.origin_slot]) { + const type = fromNode.outputs[link_info.origin_slot].type; + // 仅更新输入类型,不设置 ID 的值 + this.inputs[0].type = type; + this.inputs[0].name = type; + } else { + showAlert("Node input undefined."); + } + } + if (link_info && node.graph && slotType === 2 && isChangeConnect) { + const fromNode = node.graph._nodes.find((n) => n.id === link_info.origin_id); + if (fromNode && fromNode.inputs && fromNode.inputs[link_info.origin_slot]) { + const type = fromNode.inputs[link_info.origin_slot].type; + this.outputs[0].type = type; + this.outputs[0].name = type; + } else { + showAlert("Node output undefined."); + } + } + this.update(); + }; + + this.validateName = function (graph) { + let widgetValue = this.widgets[0].value; + if (widgetValue !== '') { + let tries = 1; + const existingValues = new Set(); + graph._nodes.forEach(otherNode => { + if (otherNode !== this && otherNode.type === 'KaySetNode') { + existingValues.add(otherNode.widgets[0].value); + } + }); + let baseValue = widgetValue; + while (existingValues.has(widgetValue)) { + widgetValue = `${baseValue}_${tries}`; + tries++; + } + this.widgets[0].value = widgetValue; + this.title = "🛜Set_" + widgetValue; + this.update(); + } + }; + + this.clone = function () { + const cloned = KaySetNode.prototype.clone.apply(this); + cloned.inputs[0].name = '*'; + cloned.inputs[0].type = '*'; + cloned.value = ''; + cloned.properties.previousName = ''; + cloned.size = cloned.computeSize(); + return cloned; + }; + + this.onAdded = function (graph) { + this.validateName(graph); + }; + + this.update = function () { + if (!this.graph) return; + const getters = this.findGetters(this.graph); + getters.forEach(getter => getter.setType(this.inputs[0].type)); + if (this.widgets[0].value) { + const gettersWithPreviousName = this.findGetters(this.graph, true); + gettersWithPreviousName.forEach(getter => getter.setName(this.widgets[0].value)); + } + const allGetters = this.graph._nodes.filter(n => n.type === "KayGetNode"); + allGetters.forEach(n => { if (n.setComboValues) n.setComboValues(); }); + }; + + this.findGetters = function (graph, checkForPreviousName) { + const name = checkForPreviousName ? this.properties.previousName : this.widgets[0].value; + return graph._nodes.filter(n => n.type === 'KayGetNode' && n.widgets[0].value === name && name !== ''); + }; + + this.isVirtualNode = true; + } + + onRemoved() { + const allGetters = this.graph._nodes.filter(n => n.type === "KayGetNode"); + allGetters.forEach(n => { if (n.setComboValues) n.setComboValues([this]); }); + } + + getExtraMenuOptions(_, options) { + this.menuEntry = this.drawConnection ? "Hide connections" : "Show connections"; + options.unshift( + { + content: this.menuEntry, + callback: () => { + this.currentGetters = this.findGetters(this.graph); + if (this.currentGetters.length === 0) return; + let linkType = this.currentGetters[0].outputs[0].type; + this.slotColor = this.canvas.default_connection_color_byType[linkType]; + this.drawConnection = !this.drawConnection; + this.canvas.setDirty(true, true); + }, + has_submenu: true, + submenu: { + title: "Color", + options: [{ + content: "Highlight", + callback: () => { + this.slotColor = "orange"; + this.canvas.setDirty(true, true); + } + }], + }, + }, + { + content: "Hide all connections", + callback: () => { + const allNodes = this.graph._nodes.filter(n => n.type === "KayGetNode" || n.type === "KaySetNode"); + allNodes.forEach(n => n.drawConnection = false); + this.drawConnection = false; + this.canvas.setDirty(true, true); + }, + } + ); + this.currentGetters = this.findGetters(this.graph); + if (this.currentGetters.length) { + let gettersSubmenu = this.currentGetters.map(getter => ({ + content: `${getter.title} id: ${getter.id}`, + callback: () => { + this.canvas.centerOnNode(getter); + this.canvas.selectNode(getter, false); + this.canvas.setDirty(true, true); + }, + })); + options.unshift({ + content: "Getters", + has_submenu: true, + submenu: { title: "GetNodes", options: gettersSubmenu } + }); + } + } + + onDrawForeground(ctx) { + if (this.drawConnection) this._drawVirtualLinks(app.canvas, ctx); + } + + _drawVirtualLinks(lGraphCanvas, ctx) { + if (!this.currentGetters?.length) return; + const title = this.getTitle ? this.getTitle() : this.title; + const title_width = ctx.measureText(title).width; + const start_node_slotpos = !this.flags.collapsed + ? [this.size[0], LiteGraph.NODE_TITLE_HEIGHT * 0.5] + : [title_width + 55, -15]; + const defaultLink = { type: 'default', color: this.slotColor }; + for (const getter of this.currentGetters) { + const end_node_slotpos = !this.flags.collapsed + ? [getter.pos[0] - this.pos[0] + this.size[0], getter.pos[1] - this.pos[1]] + : [getter.pos[0] - this.pos[0] + title_width + 50, getter.pos[1] - this.pos[1] - 30]; + lGraphCanvas.renderLink(ctx, start_node_slotpos, end_node_slotpos, defaultLink, false, null, this.slotColor, LiteGraph.RIGHT, LiteGraph.LEFT); + } + } + } + + LiteGraph.registerNodeType("KaySetNode", Object.assign(KaySetNode, { + title: "🛜Set" + })); + KaySetNode.category = "KayTool"; + + class KayGetNode extends LGraphNode { + defaultVisibility = true; + serialize_widgets = true; + drawConnection = false; + slotColor = "#FFF"; + currentSetter = null; + canvas = app.canvas; + + constructor(title) { + super(title); + + // 直接设置节点样式 + this.shape = "box"; // 设置节点形状 + this.color = "#222"; // 设置节点颜色 + this.bgcolor = "#000"; // 设置节点背景颜色 + + if (!this.properties) this.properties = {}; + this.properties.showOutputText = KayGetNode.defaultVisibility; + const node = this; + this.addWidget( + "combo", + "ID", + "", + () => this.onRename(), + { + values: () => { + const setterNodes = node.graph._nodes.filter(n => n.type === 'KaySetNode'); + return setterNodes.map(n => n.widgets[0].value).sort(); + } + } + ); + + this.addOutput("*", '*'); + + this.onConnectionsChange = function () { + this.validateLinks(); + }; + + this.setName = function (name) { + node.widgets[0].value = name; + node.onRename(); + node.serialize(); + }; + + this.onRename = function () { + const setter = this.findSetter(node.graph); + if (setter) { + const linkType = setter.inputs[0].type; + this.setType(linkType); + this.title = "🛜Get_" + setter.widgets[0].value; + } else { + this.setType('*'); + } + }; + + this.clone = function () { + const cloned = KayGetNode.prototype.clone.apply(this); + cloned.size = cloned.computeSize(); + return cloned; + }; + + this.validateLinks = function () { + if (this.outputs[0].type !== '*' && this.outputs[0].links) { + this.outputs[0].links.filter(linkId => { + const link = node.graph.links[linkId]; + return link && (link.type !== this.outputs[0].type && link.type !== '*'); + }).forEach(linkId => node.graph.removeLink(linkId)); + } + }; + + this.setType = function (type) { + this.outputs[0].name = type; + this.outputs[0].type = type; + this.validateLinks(); + }; + + this.findSetter = function (graph) { + const name = this.widgets[0].value; + return graph._nodes.find(n => n.type === 'KaySetNode' && n.widgets[0].value === name && name !== ''); + }; + + this.goToSetter = function () { + const setter = this.findSetter(this.graph); + this.canvas.centerOnNode(setter); + this.canvas.selectNode(setter, false); + }; + + this.isVirtualNode = true; + } + + getInputLink(slot) { + const setter = this.findSetter(this.graph); + if (setter) { + const slotInfo = setter.inputs[slot]; + return this.graph.links[slotInfo.link]; + } else { + const errorMessage = `No KaySetNode found for ${this.widgets[0].value} (${this.type})`; + showAlert(errorMessage); + } + } + + getExtraMenuOptions(_, options) { + const menuEntry = this.drawConnection ? "Hide connections" : "Show connections"; + options.unshift( + { + content: "Go to setter", + callback: () => this.goToSetter(), + }, + { + content: menuEntry, + callback: () => { + this.currentSetter = this.findSetter(this.graph); + if (!this.currentSetter) return; + const linkType = this.currentSetter.inputs[0].type; + this.drawConnection = !this.drawConnection; + this.slotColor = this.canvas.default_connection_color_byType[linkType]; + this.canvas.setDirty(true, true); + }, + } + ); + } + + onDrawForeground(ctx) { + if (this.drawConnection) this._drawVirtualLink(app.canvas, ctx); + } + + _drawVirtualLink(lGraphCanvas, ctx) { + if (!this.currentSetter) return; + const defaultLink = { type: 'default', color: this.slotColor }; + const start_node_slotpos = [ + this.currentSetter.pos[0] - this.pos[0], + this.currentSetter.pos[1] - this.pos[1] + ]; + const end_node_slotpos = [0, -LiteGraph.NODE_TITLE_HEIGHT * 0.5]; + lGraphCanvas.renderLink(ctx, start_node_slotpos, end_node_slotpos, defaultLink, false, null, this.slotColor); + } + } + + LiteGraph.registerNodeType("KayGetNode", Object.assign(KayGetNode, { + title: "🛜Get" + })); + KayGetNode.category = "KayTool"; + }, +}); \ No newline at end of file