add preset node

This commit is contained in:
qnsh
2025-10-12 15:22:48 +08:00
parent a3d96dbc54
commit 06e653772b
8 changed files with 338 additions and 2 deletions
+1
View File
@@ -207,3 +207,4 @@ marimo/_lsp/
__marimo__/
config.ini
config.json
.vscode/
+2
View File
@@ -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
]
-2
View File
@@ -334,5 +334,3 @@ from server import PromptServer
@PromptServer.instance.routes.get("/hello")
async def get_hello(request):
return web.json_response("hello")
+117
View File
@@ -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."
}
]
+116
View File
@@ -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)
+24
View File
@@ -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"
}
}
}
}
+25
View File
@@ -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": "字符串"
}
}
}
}
+53
View File
@@ -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;
};
}
}
});