From 06e653772be35194258b9a8ab8f86cbea277006b Mon Sep 17 00:00:00 2001 From: qnsh Date: Sun, 12 Oct 2025 15:22:48 +0800 Subject: [PATCH] add preset node --- .gitignore | 1 + __init__.py | 2 + gemini_image_node.py | 2 - gemini_image_preset.json | 117 ++++++++++++++++++++++++++++++++++ gemini_image_preset_node.py | 116 +++++++++++++++++++++++++++++++++ locales/en/nodeDefs.json | 24 +++++++ locales/zh/nodeDefs.json | 25 ++++++++ web/js/gemini_image_preset.js | 53 +++++++++++++++ 8 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 gemini_image_preset.json create mode 100644 gemini_image_preset_node.py create mode 100644 web/js/gemini_image_preset.js diff --git a/.gitignore b/.gitignore index b65f221..8193203 100644 --- a/.gitignore +++ b/.gitignore @@ -207,3 +207,4 @@ marimo/_lsp/ __marimo__/ config.ini config.json +.vscode/ \ No newline at end of file diff --git a/__init__.py b/__init__.py index 203e93a..325cc90 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ from .gemini_image_node import * +from .gemini_image_preset_node import * from typing_extensions import override class APIExtension(ComfyExtension): @@ -6,6 +7,7 @@ class APIExtension(ComfyExtension): async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ GeminiImage, + GeminiImagePreset ] diff --git a/gemini_image_node.py b/gemini_image_node.py index 207099c..65274ab 100644 --- a/gemini_image_node.py +++ b/gemini_image_node.py @@ -334,5 +334,3 @@ from server import PromptServer @PromptServer.instance.routes.get("/hello") async def get_hello(request): return web.json_response("hello") - - diff --git a/gemini_image_preset.json b/gemini_image_preset.json new file mode 100644 index 0000000..b98406c --- /dev/null +++ b/gemini_image_preset.json @@ -0,0 +1,117 @@ +[ + { + "title": "👨‍💼3D手办", + "description": "将你的照片变成可收藏的3D角色手办,配有完整的包装。", + "prompt": "turn this photo into a character figure. Behind it, place a box with the character’s image printed on it, and a computer showing the Blender modeling process on its screen. In front of the box, add a round plastic base with the character figure standing on it. set the scene indoors if possible" + }, + { + "title": "🧶钩针娃娃", + "description": "将您的图像变成一个柔软的手工钩针娃娃。", + "prompt": "Transform the subject into a handmade crocheted yarn doll with a cute, chibi-style appearance." + }, + { + "title": "💃姿势参考", + "description": "将一张图像中的姿势应用到另一张图像中的角色上。", + "prompt": "Apply the pose from the second image to the character in the first image. Render as a professional studio photograph." + }, + { + "title": "🏞️光影参考", + "description": "将一张图像中的光影应用到另一张图像上。", + "prompt": "图一换成图二打光,专业摄影" + }, + { + "title": "📸时尚杂志", + "description": "为您的照片赋予高级时尚、编辑风格的外观,堪比杂志封面。", + "prompt": "Transform the photo into a stylized, ultra-realistic fashion magazine portrait with cinematic lighting." + }, + { + "title": "💡产品渲染", + "description": "将产品草图变成专业的、照片级的3D渲染图。", + "prompt": "Turn this product sketch into a photorealistic 3D render with studio lighting." + }, + { + "title": "🗿黏土动画", + "description": "将您的图像重现为一个迷人的定格黏土场景。", + "prompt": "Recreate the image as a charming stop-motion claymation scene." + }, + { + "title": "🔶低多边形", + "description": "使用低多边形几何网格重建您的图像。", + "prompt": "Reconstruct the image using a low-polygon geometric mesh." + }, + { + "title": "⚙️蒸汽朋克", + "description": "用齿轮、黄铜和维多利亚时代的技术重新想象您的主题。", + "prompt": "Reimagine the subject with steampunk aesthetics, featuring gears, brass, and Victorian-era technology." + }, + { + "title": "🌌线稿艺术", + "description": "将您的照片简化为其基本线条,创建一个干净的草图。", + "prompt": "Turn the image into a clean, hand-drawn line art sketch." + }, + { + "title": "👾像素艺术", + "description": "将您的图像转换为复古的8位像素艺术。", + "prompt": "Redraw the image in a retro 8-bit pixel art style." + }, + { + "title": "🖌️水彩艺术", + "description": "将您的图像转换为柔和、充满活力的水彩画。", + "prompt": "Transform the image into a soft and vibrant watercolor painting." + }, + { + "title": "🎨波普艺术", + "description": "以安迪·沃霍尔的大胆风格重新想象您的图像。", + "prompt": "Reimagine the image in the style of Andy Warhol's pop art, with bold colors and screen-print effects." + }, + { + "title": "✏️涂鸦艺术", + "description": "在您的图像上覆盖好玩的手绘涂鸦。", + "prompt": "Overlay the image with playful, hand-drawn doodle-style illustrations." + }, + { + "title": "🐉奇幻艺术", + "description": "将您的图像转变为一幅史诗般的奇幻风格绘画。", + "prompt": "Transform the image into an epic fantasy-style painting, with magical elements and dramatic lighting." + }, + { + "title": "🖼️矢量艺术", + "description": "将您的照片转换为干净、可缩放的矢量艺术。", + "prompt": "Convert the photo into clean, scalable vector art with flat colors and sharp lines." + }, + { + "title": "✂️折纸艺术", + "description": "用折纸风格重建您的主题。", + "prompt": "Reconstruct the subject of the image using folded paper in an origami style." + }, + { + "title": "🏜️沙画艺术", + "description": "重新创作您的图像,仿佛它是由彩色沙子制成的。", + "prompt": "Recreate the image as if it were made from colored sand." + }, + { + "title": "👨‍🔧乐高小人仔", + "description": "为你的主体制作一个乐高小人仔,准备好玩耍。", + "prompt": "Transform the person into a LEGO minifigure, inside its packaging box." + }, + { + "title": "🥤汽水罐设计", + "description": "将您的图像包装到汽水罐上,并将其放置在精美的产品照片中。", + "prompt": "Design a soda can using this image as the main graphic, and show it in a professional product shot." + }, + { + "title": "🧸可爱毛绒玩具", + "description": "将您的主题转换成一个可爱的、柔软的毛绒玩具。", + "prompt": "Turn the person in this photo into a cute, soft plushie doll." + }, + { + "title": "🔑亚克力钥匙扣", + "description": "创建一个您的主题的可爱亚克力钥匙扣,非常适合挂在包上。", + "prompt": "Turn the subject into a cute acrylic keychain, shown attached to a bag." + }, + { + "title": "🎭动漫转Cosplay", + "description": "将动漫角色变为一张逼真的Cosplay照片。", + "prompt": "Generate a highly detailed, realistic photo of a person cosplaying the character in this illustration. Replicate the pose, expression, and framing." + } +] \ No newline at end of file diff --git a/gemini_image_preset_node.py b/gemini_image_preset_node.py new file mode 100644 index 0000000..c075822 --- /dev/null +++ b/gemini_image_preset_node.py @@ -0,0 +1,116 @@ +import io +import os +import json +import base64 +import requests +import torch +import numpy as np +from PIL import Image +from io import BytesIO +from typing_extensions import override +from comfy_api.latest import ComfyExtension, io +from .image_utils import tensor_to_base64_string + +# 公共函数,用于处理预设数据 +def load_preset_data(): + """加载预设数据从JSON配置文件""" + try: + config_path = os.path.join(os.path.dirname(__file__), "gemini_image_preset.json") + with open(config_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error loading preset config: {e}") + return [] + +def get_preset_titles(): + """获取所有预设的标题列表""" + preset_data = load_preset_data() + if preset_data: + return [preset['title'] for preset in preset_data] + return ["None"] + +def get_preset_by_title(title): + """根据title获取对应的预设数据""" + preset_data = load_preset_data() + for preset in preset_data: + if preset.get('title') == title: + return preset + return None + +class GeminiImagePreset(io.ComfyNode): + """ + 这个节点为Gemini Image API 提供预设 + """ + + @classmethod + def define_schema(cls) -> io.Schema: + """ + 返回一个包含该节点所有信息的模式(schema)。 + 一些可用类型:"Model", "Vae", "Clip", "Conditioning", "Latent", "Image", "Int", "String", "Float", "Combo"。 + 对于输出,应使用 "io.Model.Output",对于输入,可以使用 "io.Model.Input"。 + 类型可以是 "Combo" —— 这将是一个供选择的列表。 + """ + # 从配置文件加载预设 + preset_options = get_preset_titles() + + return io.Schema( + node_id="YCYY_Gemini_Image_Preset", + display_name="Gemini Image Preset", + category="YCYY/API/utils", + inputs=[ + io.Combo.Input( + id="preset", + options=preset_options, + default=preset_options[0] if preset_options and preset_options[0] != "None" else "None", + tooltip="Gemini image preset name" + ), + io.String.Input( + id="description", + multiline=True, + tooltip="Gemini image preset description" + ), + io.String.Input( + id="prompt", + multiline=True, + tooltip="Gemini image preset prompt" + ), + ], + outputs=[ + io.String.Output() + ], + description="This node provides presets for the Gemini Image API." + ) + # 执行 GeminiImagePreset 节点 + @classmethod + def execute(cls, preset, description, prompt) -> io.NodeOutput: + return (prompt,) + +WEB_DIRECTORY = "./web/js" + +# 使用 router 添加自定义 API 路由 +from aiohttp import web +from server import PromptServer + + + + +@PromptServer.instance.routes.post("/ycyy/gemini/images/preset") +async def get_preset_data(request): + """获取preset数据的API端点""" + try: + data = await request.json() + preset_title = data.get('title') + + if not preset_title: + return web.json_response({"error": "Missing preset title"}, status=400) + + preset_data = get_preset_by_title(preset_title) + + if preset_data: + return web.json_response(preset_data) + else: + return web.json_response({"error": f"Preset '{preset_title}' not found"}, status=404) + + except Exception as e: + return web.json_response({"error": f"Internal server error: {str(e)}"}, status=500) + diff --git a/locales/en/nodeDefs.json b/locales/en/nodeDefs.json index accda7e..a2bfbe0 100644 --- a/locales/en/nodeDefs.json +++ b/locales/en/nodeDefs.json @@ -29,5 +29,29 @@ "name": "String" } } + }, + "YCYY_Gemini_Image_Preset":{ + "display_name": "Gemini Image Preset", + "description": "This node provides presets for the Gemini Image API.", + "inputs": { + "preset":{ + "name": "preset", + "tooltip": "Gemini image preset name" + }, + "description":{ + "name": "description", + "tooltip": "Gemini image preset description" + }, + "prompt":{ + "name": "prompt", + "tooltip": "Gemini image preset prompt" + } + }, + "outputs": { + "0": { + "name": "String" + } + } + } } \ No newline at end of file diff --git a/locales/zh/nodeDefs.json b/locales/zh/nodeDefs.json index 1c9d063..8333f42 100644 --- a/locales/zh/nodeDefs.json +++ b/locales/zh/nodeDefs.json @@ -1,6 +1,7 @@ { "YCYY_Gemini_Image_API": { "display_name": "Gemini 图像 API", + "description": "该节点使用 Google Gemini 图像 API 生成或修改图像", "inputs": { "images": { "name": "图像", @@ -29,5 +30,29 @@ "name": "字符串" } } + }, + "YCYY_Gemini_Image_Preset":{ + "display_name": "Gemini 图像预设", + "description": "该节点为 Gemini 图像 API 提供预设值", + "inputs": { + "preset":{ + "name": "预设名称", + "tooltip": "预设名称" + }, + "description":{ + "name": "预设描述", + "tooltip": "预设描述" + }, + "prompt":{ + "name": "预设提示词", + "tooltip": "预设提示词" + } + }, + "outputs": { + "0": { + "name": "字符串" + } + } + } } \ No newline at end of file diff --git a/web/js/gemini_image_preset.js b/web/js/gemini_image_preset.js new file mode 100644 index 0000000..d83d8af --- /dev/null +++ b/web/js/gemini_image_preset.js @@ -0,0 +1,53 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +app.registerExtension({ + name: "YCYY.Gemini.Image.Preset", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeType.comfyClass === "YCYY_Gemini_Image_Preset") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated?.apply(this, arguments); + + const presetWidget = this.widgets?.find(w => w.name === "preset"); + const descriptionWidget = this.widgets?.find(w => w.name === "description"); + const promptWidget = this.widgets?.find(w => w.name === "prompt"); + + if (presetWidget && descriptionWidget && promptWidget) { + const originalCallback = presetWidget.callback; + + presetWidget.callback = async (value) => { + originalCallback?.call(this, value); + + if (!value || value === "None") return; + + try { + const response = await api.fetchApi("/ycyy/gemini/images/preset", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title: value }) + }); + + if (response.ok) { + const presetData = await response.json(); + if (presetData?.description && presetData?.prompt) { + descriptionWidget.value = presetData.description; + promptWidget.value = presetData.prompt; + this.graph?.canvas?.draw(true, true); + } + } + } catch (error) { + console.error("Error fetching preset data:", error); + } + }; + + if (presetWidget.value && presetWidget.value !== "None") { + setTimeout(() => presetWidget.callback(presetWidget.value), 50); + } + } + + return r; + }; + } + } +}); \ No newline at end of file