From 5d3834503e7fde5ab7257e509dde348fc9034528 Mon Sep 17 00:00:00 2001 From: rui40000 Date: Wed, 29 Jul 2026 19:07:01 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=AB=E6=96=B9=E5=90=91=E6=8B=86?= =?UTF-8?q?=E5=88=86=E4=B8=8D=E5=86=8D=E5=88=87=E6=96=AD=E8=A7=92=E8=89=B2?= =?UTF-8?q?=EF=BC=9B=E5=85=A8=E4=BB=93=E5=BA=93=E5=8F=82=E6=95=B0=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=E4=B8=AD=E6=96=87=20tooltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【修复】角色被格线切断(脚、手杖、飘起的斗篷被削掉) 新增 expand_beyond_cell(默认开启):格子只用来判定「这是哪个方向」, 角色的实际范围由它自身的连通区域决定,按质心归属确保邻居不混入。 实测 8/8 方向的裁剪框边缘 alpha 从 1.00(内容顶到边界=被切断) 降到 0.00,S 方向高度 326→356、E 方向宽度 150→188 把缺的部分找了回来。 代价是需要两遍扫描(先求全序列并集框再提取),耗时 4.7s→14.9s。 【规则】每个参数都必须有中文 tooltip,作为以后的统一约定 全仓库 26 个节点 169 个参数,此前缺 115 个,现已 100% 覆盖。 tooltip 写「怎么调」而不只是「是什么」:给取值区间的实际影响、 推荐值与踩坑提示(如 OpenAI/ZenMux 的地址栏不能带 :// , 素材拆分节点用于动画序列时顺序会漂移等)。 Co-Authored-By: Claude Opus 4.8 --- color_matcher_node.py | 24 +++- dialogue_extractor_node.py | 5 +- eightdir_node.py | 186 +++++++++++++++++++++------ example_workflow/八方向行走动画.json | 3 +- feynobg_node.py | 11 +- flip_node.py | 9 +- image_splitter_node.py | 16 ++- load_image_node.py | 5 +- load_image_with_name_node.py | 7 +- lucida_node.py | 11 +- markdown_image_node.py | 62 ++++++--- mask_preview_node.py | 20 ++- mask_selector_node.py | 14 +- openai_node.py | 47 ++++++- page_narration_remover_node.py | 6 +- pixelate_node.py | 12 +- qwenedit_node.py | 43 +++++-- saturation_node.py | 10 +- sdmatte_node.py | 23 +++- shot_splitter_node.py | 8 +- text_box_node.py | 5 + text_list_node.py | 16 ++- utf8_converter_node.py | 10 +- watermark_node.py | 35 ++++- zenmux/zenmux_node.py | 47 ++++++- 25 files changed, 512 insertions(+), 123 deletions(-) diff --git a/color_matcher_node.py b/color_matcher_node.py index bd1ca77..5c60357 100644 --- a/color_matcher_node.py +++ b/color_matcher_node.py @@ -30,10 +30,26 @@ class ColorMatcherNode: def INPUT_TYPES(cls): return { "required": { - "reference_image": ("IMAGE",), - "moving_image": ("IMAGE",), - "match_method": (["histogram", "mean_std", "none"], {"default": "histogram"}), - "blend_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "reference_image": ("IMAGE", { + "tooltip": "参考图:提供目标色调,本身不会被改动。" + }), + "moving_image": ("IMAGE", { + "tooltip": "待校色图:它的颜色分布会被改造成参考图的样子。" + }), + "match_method": (["histogram", "mean_std", "none"], { + "default": "histogram", + "tooltip": "匹配算法:\n" + "histogram = 逐通道直方图匹配,色调贴合最紧,\n" + " 但两图内容差异大时容易出偏色\n" + "mean_std = 只对齐均值与标准差,效果温和、不易翻车\n" + "none = 不做匹配,仅用于对照原图" + }), + "blend_factor": ("FLOAT", { + "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, + "tooltip": "匹配强度:结果在原图与完全匹配之间线性混合。\n" + "0 = 保持原样,1 = 完全套用参考图色调。\n" + "匹配过头显得失真时降到 0.5~0.8 往往更自然。" + }), } } diff --git a/dialogue_extractor_node.py b/dialogue_extractor_node.py index 5f0e065..ea21588 100644 --- a/dialogue_extractor_node.py +++ b/dialogue_extractor_node.py @@ -12,7 +12,10 @@ class DialogueExtractorNode: "required": { "input_text": ("STRING", { "multiline": True, - "default": "" + "default": "", + "tooltip": "分镜描述原文。节点会从中挑出旁白/对白部分,\n" + "画面描述、镜头调度等非台词内容会被滤掉。\n" + "输出为对白列表,可直接接配音或字幕环节。" }), }, } diff --git a/eightdir_node.py b/eightdir_node.py index 6c3232b..acdeb5b 100644 --- a/eightdir_node.py +++ b/eightdir_node.py @@ -93,6 +93,41 @@ def _drop_fragments(alpha, ratio): return np.where(keep[lab], alpha, 0.0).astype(np.float32) +def _assign_blocks(alpha_full, ybnd, xbnd, cols, frag_ratio): + """ + 全图连通标记 + 按质心把每块归属到格子。 + + 这样格子只负责回答「这是哪个方向」,角色的实际范围由它自己的连通块决定, + 因此**角色超出格子边界也不会被切**(原本严格等分会把探出去的脚、 + 飘起的斗篷直接截断)。归属按质心判定,每块只属于一个格子, + 相邻角色不会被重复计入。 + + 返回 (lab, {cell_id: [块标签...]});scipy 不可用时返回 (None, None)。 + """ + if not _HAS_SCIPY: + return None, None + solid = alpha_full > 0.1 + if not solid.any(): + return None, {} + lab, n = ndi.label(solid) + if n == 0: + return lab, {} + areas = np.bincount(lab.ravel()) + areas[0] = 0 + big = areas.max() + cents = ndi.center_of_mass(solid, lab, np.arange(1, n + 1)) + rows = len(ybnd) - 1 + out = {} + for i in range(1, n + 1): + if frag_ratio > 0 and areas[i] < big * float(frag_ratio): + continue # 碎片,丢弃 + cy, cx = cents[i - 1] + r = min(rows - 1, max(0, int(np.searchsorted(ybnd, cy, "right") - 1))) + c = min(cols - 1, max(0, int(np.searchsorted(xbnd, cx, "right") - 1))) + out.setdefault(r * cols + c, []).append(i) + return lab, out + + def _bbox(alpha, thr=0.02): """内容包围盒 (y0,y1,x0,x1),无内容时返回 None。""" m = alpha > thr @@ -173,6 +208,15 @@ class RuiEightDirSplit: "切进本格,既难看又会撑大自动裁剪的范围。\n" "0 = 不清理;与身体相连的道具不会被误删。" }), + "expand_beyond_cell": ("BOOLEAN", { + "default": True, + "tooltip": "允许角色超出格子边界(强烈建议开启)。\n" + "关闭时按格子严格切分,角色只要探出格线就会被切断\n" + "——最常见的是脚、飘起的斗篷和手杖被削掉一截。\n" + "开启后格子只用来判定「这是哪个方向」,实际范围由角色\n" + "自身的连通区域决定,因此不会被切;按质心归属,\n" + "相邻角色也不会被卷进来。" + }), "auto_crop": ("BOOLEAN", { "default": True, "tooltip": "按内容裁掉多余空白。\n" @@ -203,7 +247,8 @@ class RuiEightDirSplit: def split(self, images, grid_cols, grid_rows, empty_cells, direction_names, bg_mode, bg_threshold, edge_softness, - fragment_threshold, auto_crop, crop_padding, masks=None): + fragment_threshold, expand_beyond_cell, auto_crop, crop_padding, + masks=None): mode = _BG_MODES.get(bg_mode, "white") B, H, W, C = images.shape cols, rows = int(grid_cols), int(grid_rows) @@ -234,49 +279,110 @@ class RuiEightDirSplit: ybnd = [int(round(r * H / rows)) for r in range(rows + 1)] xbnd = [int(round(c * W / cols)) for c in range(cols + 1)] - # ---- 第一遍:切格 + 生成 alpha,同时累计每个方向的内容包围盒 ---- - per_dir = [[] for _ in range(n_dir)] - boxes = [None] * n_dir - for b in range(B): + pad = int(crop_padding) + use_expand = bool(expand_beyond_cell) and mode != "none" and _HAS_SCIPY + + def frame_alpha(b): + if mode == "white": + return _white_to_alpha(rgb_all[b], bg_threshold, edge_softness) + if mode == "keep" and a_in is not None: + return a_in[b] + return np.ones((H, W), dtype=np.float32) + + outs, notes = [], [] + + if use_expand: + # ===== 内容自适应:格子只定方向,范围由角色自身的连通块决定 ===== + # 第一遍只求包围盒(全图坐标),不留像素,避免整段序列驻留内存 + boxes = [None] * n_dir + for b in range(B): + lab, groups = _assign_blocks(frame_alpha(b), ybnd, xbnd, + cols, fragment_threshold) + if lab is None: + continue + for di, cid in enumerate(cell_ids): + blk = groups.get(cid) + if not blk: + continue + bb = _bbox(np.isin(lab, blk).astype(np.float32), 0.5) + if bb is None: + continue + boxes[di] = bb if boxes[di] is None else ( + min(boxes[di][0], bb[0]), max(boxes[di][1], bb[1]), + min(boxes[di][2], bb[2]), max(boxes[di][3], bb[3])) + + # 没有 auto_crop 时退回该格的格线范围,仍允许块超界的像素带出来 + final = [] for di, cid in enumerate(cell_ids): r, c = divmod(cid, cols) - y0, y1, x0, x1 = ybnd[r], ybnd[r + 1], xbnd[c], xbnd[c + 1] - rgb = rgb_all[b, y0:y1, x0:x1] - if mode == "white": - alpha = _white_to_alpha(rgb, bg_threshold, edge_softness) - elif mode == "keep" and a_in is not None: - alpha = a_in[b, y0:y1, x0:x1] + if auto_crop and boxes[di] is not None: + y0, y1, x0, x1 = boxes[di] + y0 = max(0, y0 - pad); x0 = max(0, x0 - pad) + y1 = min(H, y1 + pad); x1 = min(W, x1 + pad) + elif boxes[di] is not None: + y0, y1, x0, x1 = (min(ybnd[r], boxes[di][0]), + max(ybnd[r + 1], boxes[di][1]), + min(xbnd[c], boxes[di][2]), + max(xbnd[c + 1], boxes[di][3])) else: - alpha = np.ones(rgb.shape[:2], dtype=np.float32) - if mode != "none": - alpha = _drop_fragments(alpha, fragment_threshold) - per_dir[di].append((rgb, alpha)) - if auto_crop: - bb = _bbox(alpha) - if bb is not None: - boxes[di] = bb if boxes[di] is None else ( - min(boxes[di][0], bb[0]), max(boxes[di][1], bb[1]), - min(boxes[di][2], bb[2]), max(boxes[di][3], bb[3])) + y0, y1, x0, x1 = ybnd[r], ybnd[r + 1], xbnd[c], xbnd[c + 1] + final.append((y0, y1, x0, x1)) + outs.append(np.zeros((B, y1 - y0, x1 - x0, 4), dtype=np.float32)) - # ---- 第二遍:按并集包围盒统一裁剪并打包 ---- - outs, notes = [], [] - pad = int(crop_padding) - for di in range(n_dir): - frames = per_dir[di] - ch, cw = frames[0][0].shape[:2] - if auto_crop and boxes[di] is not None: - y0, y1, x0, x1 = boxes[di] - y0 = max(0, y0 - pad); x0 = max(0, x0 - pad) - y1 = min(ch, y1 + pad); x1 = min(cw, x1 + pad) - else: - y0, y1, x0, x1 = 0, ch, 0, cw - stack = np.empty((len(frames), y1 - y0, x1 - x0, 4), dtype=np.float32) - for fi, (rgb, alpha) in enumerate(frames): - stack[fi, ..., :3] = rgb[y0:y1, x0:x1] - stack[fi, ..., 3] = alpha[y0:y1, x0:x1] - outs.append(torch.from_numpy(stack)) - notes.append(f"{di + 1}.{names[di]} 格{cell_ids[di]} " - f"{x1 - x0}×{y1 - y0}") + # 第二遍按并集框提取;只保留归属本格的连通块,邻居不会混进来 + for b in range(B): + af = frame_alpha(b) + lab, groups = _assign_blocks(af, ybnd, xbnd, cols, fragment_threshold) + for di, cid in enumerate(cell_ids): + y0, y1, x0, x1 = final[di] + outs[di][b, ..., :3] = rgb_all[b, y0:y1, x0:x1] + blk = groups.get(cid) if groups else None + if not blk: + continue + m = np.isin(lab[y0:y1, x0:x1], blk) + outs[di][b, ..., 3] = af[y0:y1, x0:x1] * m + + for di in range(n_dir): + y0, y1, x0, x1 = final[di] + notes.append(f"{di + 1}.{names[di]} 格{cell_ids[di]} " + f"{x1 - x0}×{y1 - y0}") + outs[di] = torch.from_numpy(outs[di]) + else: + # ===== 严格按格线切分(角色探出格线会被截断)===== + per_dir = [[] for _ in range(n_dir)] + boxes = [None] * n_dir + for b in range(B): + af = frame_alpha(b) + for di, cid in enumerate(cell_ids): + r, c = divmod(cid, cols) + y0, y1, x0, x1 = ybnd[r], ybnd[r + 1], xbnd[c], xbnd[c + 1] + rgb = rgb_all[b, y0:y1, x0:x1] + alpha = af[y0:y1, x0:x1] + if mode != "none": + alpha = _drop_fragments(alpha, fragment_threshold) + per_dir[di].append((rgb, alpha)) + if auto_crop: + bb = _bbox(alpha) + if bb is not None: + boxes[di] = bb if boxes[di] is None else ( + min(boxes[di][0], bb[0]), max(boxes[di][1], bb[1]), + min(boxes[di][2], bb[2]), max(boxes[di][3], bb[3])) + for di in range(n_dir): + frames = per_dir[di] + ch, cw = frames[0][0].shape[:2] + if auto_crop and boxes[di] is not None: + y0, y1, x0, x1 = boxes[di] + y0 = max(0, y0 - pad); x0 = max(0, x0 - pad) + y1 = min(ch, y1 + pad); x1 = min(cw, x1 + pad) + else: + y0, y1, x0, x1 = 0, ch, 0, cw + stack = np.empty((len(frames), y1 - y0, x1 - x0, 4), dtype=np.float32) + for fi, (rgb, alpha) in enumerate(frames): + stack[fi, ..., :3] = rgb[y0:y1, x0:x1] + stack[fi, ..., 3] = alpha[y0:y1, x0:x1] + outs.append(torch.from_numpy(stack)) + notes.append(f"{di + 1}.{names[di]} 格{cell_ids[di]} " + f"{x1 - x0}×{y1 - y0}") info = (f"输入 {B} 帧 {W}×{H} → {cols}×{rows} 网格," f"空格 {sorted(empties) if empties else '无'}," diff --git a/example_workflow/八方向行走动画.json b/example_workflow/八方向行走动画.json index 4feebcf..2377843 100644 --- a/example_workflow/八方向行走动画.json +++ b/example_workflow/八方向行走动画.json @@ -165,6 +165,7 @@ 1.0, 0.05, true, + true, 8 ], "color": "#232", @@ -927,7 +928,7 @@ "outputs": [], "properties": {}, "widgets_values": [ - "八方向行走动画 —— 完整管线\n\n【前置步骤(在本工作流之外完成)】\n 角色图 →(GPTimage2)→ 八方向静态图 →(Seedance 首尾帧)→ 循环行走视频\n 视频每一帧上都排布着 8 个朝向的角色。\n\n【本工作流做的事】\n ① VHS「Load Video」把视频转成序列帧\n · force_rate 就是帧率选择。设 0 用源视频帧率;\n 设 12 可减半出帧、素材更小,走路动画通常 8~12 帧一循环就够\n · frame_load_cap 可限制总帧数,先小批量试参数很有用\n ② 本仓库「八方向序列拆分」:切格 + 编号 + 按方向分组,一次给出 8 条序列\n ③ 每个方向各接一个 VHS「Video Combine」出透明 webm\n ④ 每个方向各接一个 SaveImage 出透明 PNG 序列帧\n\n【拆分节点的关键参数】\n · grid_cols / grid_rows / empty_cells\n 3×3 中间留空即 empty_cells=4。序号行优先、从 0 开始。\n · direction_names\n 按「跳过空格后的先后顺序」命名,默认对应下面这个排布:\n SW S SE ← 行1:面向观众\n W E ← 行2:侧面\n NW N NE ← 行3:背对观众\n 与你的素材不符就直接改这行,名字只影响标注不影响画面。\n · bg_mode / bg_threshold\n 白底转透明。只把「与画面边缘相连」的白色判为背景,\n 所以角色身上的白衬衫、白高光不会被掏空。\n 背景没扣净就调低阈值,角色边缘被啃就调高。\n · auto_crop\n 裁掉多余空白。裁剪框取该方向所有帧的并集,\n 因此整条序列尺寸一致(能合成视频),角色也不会在帧间跳动。\n\n【为什么不用连通区域拆分(本仓库的「素材拆分」节点)】\n 那个节点按包围盒排序,角色走动时位置浮动,一旦跨过排序行界方向就会错乱;\n 且每个 sprite 按各自 bbox 裁剪、尺寸不一,无法直接合成视频。\n 固定网格没有这两个问题 —— 但它更适合单张静态合图,两者各有用途。\n\n【webm 透明的关键】\n Video Combine 的 format 必须选 video/webm,且 pix_fmt 选 yuva420p。\n 注意:alpha 存放在 WebM 的独立边带里,所以用 ffprobe 看主流会显示\n yuv420p,这是正常的。播放器/引擎(走 libvpx 解码)能正确读到透明通道;\n 若要用 ffmpeg 自行验证,需显式加 -c:v libvpx-vp9 解码,\n 否则内置 vp9 解码器不会输出 alpha,会让人误以为透明丢了。\n\n【实测(97 帧 834×1112 的素材)】\n 拆分耗时约 4 秒,8 个方向各 97 帧,尺寸 172×370 ~ 206×327。" + "八方向行走动画 —— 完整管线\n\n【前置步骤(在本工作流之外完成)】\n 角色图 →(GPTimage2)→ 八方向静态图 →(Seedance 首尾帧)→ 循环行走视频\n 视频每一帧上都排布着 8 个朝向的角色。\n\n【本工作流做的事】\n ① VHS「Load Video」把视频转成序列帧\n · force_rate 就是帧率选择。设 0 用源视频帧率;\n 设 12 可减半出帧、素材更小,走路动画通常 8~12 帧一循环就够\n · frame_load_cap 可限制总帧数,先小批量试参数很有用\n ② 本仓库「八方向序列拆分」:切格 + 编号 + 按方向分组,一次给出 8 条序列\n ③ 每个方向各接一个 VHS「Video Combine」出透明 webm\n ④ 每个方向各接一个 SaveImage 出透明 PNG 序列帧\n\n【拆分节点的关键参数】\n · grid_cols / grid_rows / empty_cells\n 3×3 中间留空即 empty_cells=4。序号行优先、从 0 开始。\n · direction_names\n 按「跳过空格后的先后顺序」命名,默认对应下面这个排布:\n SW S SE ← 行1:面向观众\n W E ← 行2:侧面\n NW N NE ← 行3:背对观众\n 与你的素材不符就直接改这行,名字只影响标注不影响画面。\n · bg_mode / bg_threshold\n 白底转透明。只把「与画面边缘相连」的白色判为背景,\n 所以角色身上的白衬衫、白高光不会被掏空。\n 背景没扣净就调低阈值,角色边缘被啃就调高。\n · expand_beyond_cell(务必开启)\n 允许角色超出格子边界。关闭时按格线严格切分,角色只要探出格线\n 就会被切断(最典型的是脚被削掉)。开启后格子只用来判定方向,\n 实际范围由角色自身的连通区域决定。\n · auto_crop\n 裁掉多余空白。裁剪框取该方向所有帧的并集,\n 因此整条序列尺寸一致(能合成视频),角色也不会在帧间跳动。\n\n【为什么不用连通区域拆分(本仓库的「素材拆分」节点)】\n 那个节点按包围盒排序,角色走动时位置浮动,一旦跨过排序行界方向就会错乱;\n 且每个 sprite 按各自 bbox 裁剪、尺寸不一,无法直接合成视频。\n 固定网格没有这两个问题 —— 但它更适合单张静态合图,两者各有用途。\n\n【webm 透明的关键】\n Video Combine 的 format 必须选 video/webm,且 pix_fmt 选 yuva420p。\n 注意:alpha 存放在 WebM 的独立边带里,所以用 ffprobe 看主流会显示\n yuv420p,这是正常的。播放器/引擎(走 libvpx 解码)能正确读到透明通道;\n 若要用 ffmpeg 自行验证,需显式加 -c:v libvpx-vp9 解码,\n 否则内置 vp9 解码器不会输出 alpha,会让人误以为透明丢了。\n\n【实测(97 帧 834×1112 的素材)】\n 拆分耗时约 4 秒,8 个方向各 97 帧,尺寸 172×370 ~ 206×327。" ], "color": "#432", "bgcolor": "#653" diff --git a/feynobg_node.py b/feynobg_node.py index 81970d1..2b24c29 100644 --- a/feynobg_node.py +++ b/feynobg_node.py @@ -207,7 +207,10 @@ class RuiFeyNobg: def INPUT_TYPES(cls): return { "required": { - "image": ("IMAGE",), + "image": ("IMAGE", { + "tooltip": "待抠图的图像,支持批量(逐张推理,避免显存峰值过高)。\n" + "RGBA 输入会自动丢弃 alpha 只取 RGB。" + }), "model_name": (_list_models(), { "tooltip": "放在 models/nobg 下的模型目录(需含 config.json 与 " "model.safetensors)。\n" @@ -225,7 +228,11 @@ class RuiFeyNobg: "但 fp16 显存减半且明显更快,推荐优先用 fp16。\n" "保留 fp32 作默认,是为老显卡上半精度异常时有个退路。" }), - "device": (["auto", "cpu"], {"default": "auto"}), + "device": (["auto", "cpu"], { + "default": "auto", + "tooltip": "auto = 有显卡就用显卡。\n" + "cpu 会慢十倍以上,仅在显存实在不够时用。" + }), }, "optional": { "alpha_threshold": ("FLOAT", { diff --git a/flip_node.py b/flip_node.py index 3bdb4da..730efcf 100644 --- a/flip_node.py +++ b/flip_node.py @@ -11,9 +11,14 @@ class FlipNode: def INPUT_TYPES(cls): return { "required": { - "image": ("IMAGE",), + "image": ("IMAGE", { + "tooltip": "要翻转的图像。" + }), "flip_direction": (["水平", "垂直"], { - "default": "水平" + "default": "水平", + "tooltip": "翻转方向:\n" + "水平 = 左右镜像(做角色朝向的反向素材常用)\n" + "垂直 = 上下颠倒" }), }, } diff --git a/image_splitter_node.py b/image_splitter_node.py index 507f1a7..d9d24e9 100644 --- a/image_splitter_node.py +++ b/image_splitter_node.py @@ -12,7 +12,11 @@ class SpriteSplitter: def INPUT_TYPES(cls): return { "required": { - "图像": ("IMAGE",), + "图像": ("IMAGE", { + "tooltip": "待拆分的合图,需自带透明通道(背景为透明)。\n" + "节点按 alpha 的连通区域找出各个独立素材并逐个裁出。\n" + "白底图请先接抠图节点转成透明再进来,否则整图会被当成一块。" + }), "最小面积过滤(像素数)": ("INT", { "default": 100, "min": 1, @@ -162,7 +166,11 @@ class SpriteSplitterRGBA: def INPUT_TYPES(cls): return { "required": { - "图像": ("IMAGE",), + "图像": ("IMAGE", { + "tooltip": "待拆分的合图,需自带透明通道(背景为透明)。\n" + "节点按 alpha 的连通区域找出各个独立素材并逐个裁出。\n" + "白底图请先接抠图节点转成透明再进来,否则整图会被当成一块。" + }), "最小面积过滤(像素数)": ("INT", { "default": 100, "min": 1, "max": 50000, "step": 1, "tooltip": "最小面积过滤。" @@ -173,6 +181,10 @@ class SpriteSplitterRGBA: }), "排序方式": (["从左到右-从上到下", "从上到下-从左到右", "面积从大到小", "面积从小到大"],{ "default": "从左到右-从上到下", + "tooltip": "输出顺序。\n" + "⚠ 逐帧处理动画序列时慎用:素材位置一旦浮动跨过内部\n" + "行/列分界,顺序就会在帧间改变。那种场景请改用\n" + "「八方向序列拆分」节点(按固定网格定位,顺序恒定)。" }), "seed": ("INT", { "default": 0, "min": 0, "max": 0xffffffffffffffff, diff --git a/load_image_node.py b/load_image_node.py index 1d63731..099e540 100644 --- a/load_image_node.py +++ b/load_image_node.py @@ -14,7 +14,10 @@ class LoadImageByPathNode: "required": { "image_path": ("STRING", { "default": "E:\\ComfyUIModels\\input\\10\\1.png", - "multiline": False + "multiline": False, + "tooltip": "图像文件的完整路径,可指向 ComfyUI 目录之外的任意位置。\n" + "适合批量处理散落在各处的素材,省去先拷进 input 目录。\n" + "Windows 路径直接粘贴即可,反斜杠无需转义。" }), }, } diff --git a/load_image_with_name_node.py b/load_image_with_name_node.py index 7a5e814..960014b 100644 --- a/load_image_with_name_node.py +++ b/load_image_with_name_node.py @@ -14,7 +14,12 @@ class LoadImageWithNameNode: input_dir = folder_paths.get_input_directory() files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] return {"required": - {"image": (sorted(files), {"image_upload": True})}, + {"image": (sorted(files), { + "image_upload": True, + "tooltip": "从 input 目录选择图像,或点下方按钮上传。\n" + "与原生加载节点的区别:额外输出不带后缀的文件名,\n" + "便于让输出文件沿用原名,批量处理时不会张冠李戴。" + })}, } CATEGORY = "Rui-Node🐶/文件存储与加载📁" diff --git a/lucida_node.py b/lucida_node.py index 84794fd..296bd81 100644 --- a/lucida_node.py +++ b/lucida_node.py @@ -159,7 +159,10 @@ class RuiLucida: def INPUT_TYPES(cls): return { "required": { - "image": ("IMAGE",), + "image": ("IMAGE", { + "tooltip": "待抠图的图像,支持批量(逐张推理,避免显存峰值过高)。\n" + "RGBA 输入会自动丢弃 alpha 只取 RGB。" + }), "model_name": (_list_ckpts(), { "tooltip": "放在 models/lucida 下的权重(.safetensors)。\n" "未找到时首次运行会自动从 HuggingFace 下载" @@ -170,7 +173,11 @@ class RuiLucida: "tooltip": "fp16 显存减半、速度更快,实测与 fp32 输出基本一致。\n" "遇到黑图或异常时改回 fp32。" }), - "device": (["auto", "cpu"], {"default": "auto"}), + "device": (["auto", "cpu"], { + "default": "auto", + "tooltip": "auto = 有显卡就用显卡。\n" + "cpu 会慢十倍以上,仅在显存实在不够时用。" + }), }, "optional": { "alpha_threshold": ("FLOAT", { diff --git a/markdown_image_node.py b/markdown_image_node.py index 300cb77..ab11b7e 100644 --- a/markdown_image_node.py +++ b/markdown_image_node.py @@ -82,29 +82,59 @@ class MarkdownToImageNode: font_labels = list(scan_fonts().keys()) default_font = DEFAULT_FONT_LABEL if DEFAULT_FONT_LABEL in font_labels \ else font_labels[0] - size_opt = {"default": "auto", "multiline": False} + def _sz(desc): + """auto 类参数:填 auto 走自动推算,填数值则强制指定。""" + return {"default": "auto", "multiline": False, + "tooltip": desc + "\n填 auto 由节点自动推算,填数值则强制指定。"} + return { "required": { - "markdown": ("STRING", {"default": _DEMO_MD, "multiline": True}), + "markdown": ("STRING", { + "default": _DEMO_MD, "multiline": True, + "tooltip": "Markdown 原文。支持标题、粗斜体、删除线、行内代码、\n" + "代码块、引用、列表、任务清单、表格、链接与彩色 Emoji。\n" + "⚠ 别用 WAS 的「Text Multiline」喂本节点——它会把 # 开头的\n" + "标题行当注释删掉。请用本仓库的「多行文本框(原样输出)」。" + }), "size_preset": (list(_SIZE_PRESETS.keys()), { "default": "1080×1440 竖版 3:4", + "tooltip": "输出尺寸预设。选 custom 时才使用下方的宽高,\n" + "选其他预设时下方宽高会被忽略。" + }), + "width": ("INT", { + "default": 1080, "min": 64, "max": 8192, "step": 8, + "tooltip": "自定义宽度,仅在预设选 custom 时生效。" + }), + "height": ("INT", { + "default": 1440, "min": 64, "max": 8192, "step": 8, + "tooltip": "自定义高度,仅在预设选 custom 时生效。\n" + "内容排不下时会自动缩小字号来适配,而不是裁切。" + }), + "font": (font_labels, { + "default": default_font, + "tooltip": "正文字体,列表来自 Ruinode/font 目录(ttf/otf/ttc)。\n" + "放入新字体后刷新页面即出现。同族粗体文件(如 msyhbd)\n" + "会自动配对用于渲染粗体,没有粗体文件时用描边模拟。" + }), + "theme": (list(_THEMES.keys()), { + "default": "浅色 light", + "tooltip": "配色主题:浅色适合分享长图,深色适合代码类内容,\n" + "米色接近纸质阅读观感。" }), - "width": ("INT", {"default": 1080, "min": 64, "max": 8192, "step": 8}), - "height": ("INT", {"default": 1440, "min": 64, "max": 8192, "step": 8}), - "font": (font_labels, {"default": default_font}), - "theme": (list(_THEMES.keys()), {"default": "浅色 light"}), }, "optional": { - "body_size": ("STRING", dict(size_opt)), - "h1_size": ("STRING", dict(size_opt)), - "h2_size": ("STRING", dict(size_opt)), - "h3_size": ("STRING", dict(size_opt)), - "h4_size": ("STRING", dict(size_opt)), - "h5_size": ("STRING", dict(size_opt)), - "h6_size": ("STRING", dict(size_opt)), - "letter_spacing": ("STRING", dict(size_opt)), - "line_spacing": ("STRING", dict(size_opt)), - "max_chars_per_line": ("STRING", dict(size_opt)), + "body_size": ("STRING", _sz("正文字号(像素)。")), + "h1_size": ("STRING", _sz("一级标题字号。auto 时为正文的 2.0 倍。")), + "h2_size": ("STRING", _sz("二级标题字号。auto 时为正文的 1.5 倍。")), + "h3_size": ("STRING", _sz("三级标题字号。auto 时为正文的 1.25 倍。")), + "h4_size": ("STRING", _sz("四级标题字号。auto 时与正文同大。")), + "h5_size": ("STRING", _sz("五级标题字号。auto 时为正文的 0.875 倍。")), + "h6_size": ("STRING", _sz("六级标题字号。auto 时为正文的 0.85 倍。")), + "letter_spacing": ("STRING", _sz("字间距(像素),可为负让字更紧凑。")), + "line_spacing": ("STRING", _sz( + "行距倍数,1.6 左右最接近阅读器观感;\n值越大越松散。")), + "max_chars_per_line": ("STRING", _sz( + "单行最多排多少字,超出即换行。\n调小可让版面更窄、更易读。")), }, } diff --git a/mask_preview_node.py b/mask_preview_node.py index 98826f0..551244e 100644 --- a/mask_preview_node.py +++ b/mask_preview_node.py @@ -25,12 +25,19 @@ class RuiMaskPreview: def INPUT_TYPES(cls): return { "required": { - "image": ("IMAGE",), - "mask": ("MASK",), + "image": ("IMAGE", { + "tooltip": "作为底图的原图。" + }), + "mask": ("MASK", { + "tooltip": "要可视化的遮罩,会以彩色半透明叠在底图上。\n" + "尺寸与底图不一致时会自动缩放对齐。" + }), "mask_color": ( - ["红色 / Red", "绿色 / Green", "蓝色 / Blue", "黄色 / Yellow", + ["红色 / Red", "绿色 / Green", "蓝色 / Blue", "黄色 / Yellow", "青色 / Cyan", "品红 / Magenta", "白色 / White"], - {"default": "红色 / Red"} + {"default": "红色 / Red", + "tooltip": "叠加色。挑一个与画面主色反差大的更容易看清遮罩边界,\n" + "例如人像多用红或青,绿植场景避开绿色。"} ), }, "optional": { @@ -38,7 +45,10 @@ class RuiMaskPreview: "default": 0.5, "min": 0.0, "max": 1.0, - "step": 0.05 + "step": 0.05, + "tooltip": "叠加不透明度。\n" + "0 = 只见原图,1 = 只见纯色块。\n" + "看边缘细节用 0.3~0.5,确认覆盖范围用 0.7 以上。" }), } } diff --git a/mask_selector_node.py b/mask_selector_node.py index 4d2b68d..74f4818 100644 --- a/mask_selector_node.py +++ b/mask_selector_node.py @@ -12,14 +12,22 @@ class MaskSelector: def INPUT_TYPES(cls): return { "required": { - "masks": ("MASK",), + "masks": ("MASK", { + "tooltip": "待筛选的遮罩批次(N,H,W)。单张遮罩也可以,会自动补成批次。" + }), "sort_method": (["按面积排序 / By Area", "从左到右 / Left to Right", "从上到下 / Top to Bottom"], { - "default": "按面积排序 / By Area" + "default": "按面积排序 / By Area", + "tooltip": "先按此规则排序,再用下面的编号取第几个。\n" + "按面积:从大到小,取主体用它最稳\n" + "从左到右 / 从上到下:按遮罩质心的坐标排\n" + "空遮罩一律排到末尾。" }), "index": ("INT", { "default": 1, "min": 1, - "step": 1 + "step": 1, + "tooltip": "取排序后的第几个遮罩,从 1 开始。\n" + "超出总数会自动夹到最后一个,并在 info 里标记 index_clamped。" }), } } diff --git a/openai_node.py b/openai_node.py index 1453155..0f9f660 100644 --- a/openai_node.py +++ b/openai_node.py @@ -78,66 +78,99 @@ class OpenAINode: "required": { # 下拉框只含纯英文单词,不含 :// ,杜绝被前端吞掉 "protocol": (["https", "http"], { - "default": "https" + "default": "https", + "tooltip": "接口协议。之所以单独做成下拉而不写进地址栏:\n" + "ComfyUI 前端会吞掉文本框里的 \"://\" 片段,\n" + "协议只能由后端拼接。" }), # 默认值不含任何协议前缀,杜绝被前端吞掉 "api_url": ("STRING", { "default": "api.openai.com/v1/chat/completions", "multiline": False, + "tooltip": "接口地址,**不要带 http:// 或 https://**(协议见上方下拉)。\n" + "只填域名和路径,例如 api.openai.com/v1/chat/completions。\n" + "第三方中转填对应的域名即可。" }), "api_key": ("STRING", { "default": "", "multiline": False, + "tooltip": "API 密钥。\n" + "⚠ 工作流会连同此值一起保存,分享 json 前记得清空。" }), "model": ("STRING", { "default": "gpt-4o", "multiline": False, + "tooltip": "模型名,按服务商文档填写。\n" + "要传图就必须选支持视觉的型号,否则图会被忽略或直接报错。" }), "system_prompt": ("STRING", { "default": "You are a helpful assistant.", "multiline": True, + "tooltip": "系统提示词:设定模型的角色与总体行为准则。\n" + "输出格式要求(如「只返回 JSON」)写在这里比写在用户\n" + "提示词里更稳定。" }), "user_prompt": ("STRING", { "default": "", "multiline": True, + "tooltip": "用户提示词:这一次具体要模型做什么。\n" + "接了图像时,在这里描述针对图像的任务。" }), "seed": ("INT", { "default": 0, "min": 0, "max": 0xffffffffffffffff, + "tooltip": "随机种子。多数服务商并不真正支持复现,\n" + "这里主要用于强制节点重新执行(改了它就不会走缓存)。" }), }, "optional": { - "image_1": ("IMAGE",), - "image_2": ("IMAGE",), - "image_3": ("IMAGE",), - "image_4": ("IMAGE",), - "image_5": ("IMAGE",), - "image_6": ("IMAGE",), + "image_1": ("IMAGE", { + "tooltip": "要一并发给模型的图像 1(需模型支持视觉)。\n" + "会按下方的最大边长压缩后转 base64 提交。" + }), + "image_2": ("IMAGE", {"tooltip": "图像 2。"}), + "image_3": ("IMAGE", {"tooltip": "图像 3。"}), + "image_4": ("IMAGE", {"tooltip": "图像 4。"}), + "image_5": ("IMAGE", {"tooltip": "图像 5。"}), + "image_6": ("IMAGE", {"tooltip": "图像 6。图越多越贵、越慢。"}), "temperature": ("FLOAT", { "default": 0.3, "min": 0.0, "max": 2.0, "step": 0.1, + "tooltip": "采样温度:越低越稳定保守,越高越发散。\n" + "要结构化/可解析的输出用 0~0.3;\n" + "要创意文案用 0.7~1.0。超过 1.2 常出现胡言乱语。" }), "max_tokens": ("INT", { "default": 500, "min": 1, "max": 8192, + "tooltip": "回复的最大长度上限。\n" + "设小了会把回答从中间截断,长文任务记得调大。" }), "detail": (["low", "high", "auto"], { "default": "auto", + "tooltip": "图像细节级别(OpenAI 视觉参数):\n" + "low 便宜快速,只看大致内容;\n" + "high 会切块细看,认小字/细节更准但更贵;\n" + "auto 由服务端决定。" }), "image_max_size": ("INT", { "default": 1024, "min": 256, "max": 4096, "step": 64, + "tooltip": "上传前把图缩放到的最大边长。\n" + "调小可显著省钱提速,但小字与细节会看不清。" }), # 代理地址也不带协议前缀,只填 IP:端口 即可 "proxy_url": ("STRING", { "default": "", "multiline": False, + "tooltip": "HTTP 代理,**同样不要带协议前缀**,只填 IP:端口,\n" + "例如 127.0.0.1:7890。留空表示直连。" }), }, } diff --git a/page_narration_remover_node.py b/page_narration_remover_node.py index 818913d..d09d5b3 100644 --- a/page_narration_remover_node.py +++ b/page_narration_remover_node.py @@ -15,7 +15,11 @@ class PageNarrationRemoverNode: "required": { "input_text": ("STRING", { "multiline": True, - "default": "" + "default": "", + "tooltip": "原始文本(多行)。节点会删掉形如\n" + " 页面旁白:\"……\"\n" + "这样单独成行的旁白,其余内容原样保留。\n" + "只处理独占一行的旁白,行内夹带的不会被误删。" }), }, } diff --git a/pixelate_node.py b/pixelate_node.py index aff272d..c707cf0 100644 --- a/pixelate_node.py +++ b/pixelate_node.py @@ -66,7 +66,10 @@ class RuiPixelate: def INPUT_TYPES(cls): return { "required": { - "image": ("IMAGE",), + "image": ("IMAGE", { + "tooltip": "待像素化的图像。批量输入时若各图比例不同,\n" + "会统一到首张的输出尺寸(ComfyUI 的 IMAGE 必须同形状)。" + }), "mode": (_MODES, { "default": "按目标宽度", "tooltip": "按目标宽度:普通图转像素画,直接给输出宽度\n" @@ -132,7 +135,12 @@ class RuiPixelate: "tooltip": "遮罩二值化阈值,高于它算不透明。\n" "设为 0 则保留灰度遮罩(不二值化)。" }), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}), + "seed": ("INT", { + "default": 0, "min": 0, "max": 0xFFFFFFFF, + "tooltip": "随机种子。影响 k-means 调色板的初始化与随机噪声抖动;\n" + "同一张图换种子会得到略有差异的配色,可多试几个挑顺眼的。\n" + "固定调色板与 Bayer 抖动不受它影响。" + }), }, } diff --git a/qwenedit_node.py b/qwenedit_node.py index bb8a349..90f6177 100644 --- a/qwenedit_node.py +++ b/qwenedit_node.py @@ -16,37 +16,60 @@ class QwenEditNode: def INPUT_TYPES(cls): return { "required": { - "image1": ("IMAGE",), - "image2": ("IMAGE",), - "image3": ("IMAGE",), - "image4": ("IMAGE",), + "image1": ("IMAGE", { + "tooltip": "参考图 1(主参考)。控制模式决定它以什么方式影响生成,\n" + "例如 reference 取整体风格、pose 取人物姿态。" + }), + "image2": ("IMAGE", { + "tooltip": "参考图 2。多张参考会一并提交,用于综合约束生成结果。" + }), + "image3": ("IMAGE", { + "tooltip": "参考图 3。" + }), + "image4": ("IMAGE", { + "tooltip": "参考图 4。" + }), "api_key": ("STRING", { "default": "", - "multiline": False + "multiline": False, + "tooltip": "阿里云百炼(DashScope)的 API Key。\n" + "⚠ 工作流会连同此值一起保存,分享 json 前记得清空。" }), "base_url": ("STRING", { "default": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image-generation/generation", - "multiline": False + "multiline": False, + "tooltip": "接口地址,一般不用改。\n" + "走代理或私有网关时替换成对应地址。" }), "seed": ("INT", { "default": -1, "min": -1, - "max": 2147483647 + "max": 2147483647, + "tooltip": "随机种子。-1 表示每次随机;\n" + "填固定值可复现同一结果,便于对比参数改动的影响。" }), "control_mode": (["reference", "sketch", "scribble", "pose", "canny", "depth", "hed", "mlsd", "normal", "seg"], { - "default": "reference" + "default": "reference", + "tooltip": "参考图的约束方式:\n" + "reference 整体风格参考(最常用)\n" + "pose 取人物姿态 / depth 取空间深度 / seg 取区域分割\n" + "canny·hed·mlsd·scribble·sketch 取不同粗细的线稿\n" + "normal 取法线朝向。约束越强,构图越贴近参考图。" }), "width": ("INT", { "default": 1024, "min": 512, "max": 2048, - "step": 8 + "step": 8, + "tooltip": "输出宽度。与高度共同决定画幅比例,\n" + "比例与参考图差异过大时构图容易变形。" }), "height": ("INT", { "default": 1024, "min": 512, "max": 2048, - "step": 8 + "step": 8, + "tooltip": "输出高度。" }), }, } diff --git a/saturation_node.py b/saturation_node.py index d6b4b14..a3c04c5 100644 --- a/saturation_node.py +++ b/saturation_node.py @@ -11,12 +11,18 @@ class SaturationNode: def INPUT_TYPES(cls): return { "required": { - "image": ("IMAGE",), + "image": ("IMAGE", { + "tooltip": "要调整饱和度的图像。" + }), "saturation": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 5.0, - "step": 0.1 + "step": 0.1, + "tooltip": "饱和度系数:\n" + "0.0 = 完全去色(黑白)\n" + "1.0 = 保持原样\n" + "1.0~2.0 = 色彩逐渐浓郁,超过 2.0 容易溢出失真" }), }, } diff --git a/sdmatte_node.py b/sdmatte_node.py index 3d39a5d..bc2b99a 100644 --- a/sdmatte_node.py +++ b/sdmatte_node.py @@ -168,7 +168,11 @@ class RuiSDMatteLoader: "tooltip": "官方测试配置为 fp32(amp.enabled=False)。\n" "fp16 省显存但 SD 2.1 的 VAE 在半精度下容易溢出,可能出现黑图或噪点。" }), - "device": (["auto", "cpu"], {"default": "auto"}), + "device": (["auto", "cpu"], { + "default": "auto", + "tooltip": "auto = 有显卡就用显卡。\n" + "SDMatte 体量大,cpu 推理会慢到不实用,仅作兜底。" + }), "attention_slicing": ("BOOLEAN", { "default": True, "tooltip": "分片计算注意力。1024 分辨率下显存峰值从约 15.5GB 降到 9.1GB,\n" @@ -208,8 +212,14 @@ class RuiSDMatte: def INPUT_TYPES(cls): return { "required": { - "sdmatte_model": ("SDMATTE_MODEL",), - "image": ("IMAGE",), + "sdmatte_model": ("SDMATTE_MODEL", { + "tooltip": "由「SDMatte 加载器」输出的模型。\n" + "加载器有缓存,多个抠图节点可共用同一个加载器。" + }), + "image": ("IMAGE", { + "tooltip": "待抠图的原图。RGBA 输入会自动丢弃 alpha 只取 RGB\n" + "(VAE 编码器只收 3 通道,多出的 alpha 不能当颜色喂进去)。" + }), "mask": ("MASK", { "tooltip": "指示要抠哪个目标的提示掩码,不必精确,粗略覆盖主体即可。" }), @@ -250,7 +260,12 @@ class RuiSDMatte: "再逐像素取最大值合成提示图 —— 即每个点的影响半径。\n" "官方训练用 25,测试期用 25+10=35,故默认 35。" }), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}), + "seed": ("INT", { + "default": 0, "min": 0, "max": 0xFFFFFFFF, + "tooltip": "随机种子,仅在 prompt_type=point_mask 时真正起作用\n" + "(决定在提示区域里随机取哪 10 个点)。\n" + "其余提示类型下改它不会改变结果。" + }), }, } diff --git a/shot_splitter_node.py b/shot_splitter_node.py index a0ac109..c7ed16d 100644 --- a/shot_splitter_node.py +++ b/shot_splitter_node.py @@ -11,7 +11,9 @@ class ShotSplitterNode: "required": { "input_text": ("STRING", { "multiline": True, - "default": "" + "default": "", + "tooltip": "多分镜脚本原文。节点会识别「镜头1 / 分镜2 / Shot 3」\n" + "这类编号标记,把整段脚本拆成一条条独立的分镜描述。" }), }, "optional": { @@ -20,12 +22,16 @@ class ShotSplitterNode: "min": 0, "max": 100, "step": 1, + "tooltip": "从第几号镜头开始取,0 表示从头开始。\n" + "用于跳过前面已经出过图的镜头。" }), "shot_count": ("INT", { "default": 0, "min": 0, "max": 100, "step": 1, + "tooltip": "取多少个镜头,0 表示一直取到结尾。\n" + "与上面的起始号配合,可以分批处理长脚本。" }), }, } diff --git a/text_box_node.py b/text_box_node.py index eb9737b..c8c187a 100644 --- a/text_box_node.py +++ b/text_box_node.py @@ -22,6 +22,11 @@ class RuiTextBoxNode: "multiline": True, # 关闭动态提示词处理,杜绝 {a|b}、通配符等被前端改写 "dynamicPrompts": False, + "tooltip": "多行文本,一字不动地原样输出。\n" + "不删注释行、不做动态提示词/通配符/token 替换。\n\n" + "用来替代 WAS 的「Text Multiline」——那个节点会把\n" + "「#」开头的行当注释删掉,喂 Markdown 时标题会凭空消失。\n" + "承载 Markdown、代码等格式敏感的文本请用本节点。" }), }, } diff --git a/text_list_node.py b/text_list_node.py index b00fc70..4ac7650 100644 --- a/text_list_node.py +++ b/text_list_node.py @@ -12,25 +12,31 @@ class TextListNode: "required": { "text1": ("STRING", { "multiline": True, - "default": "" + "default": "", + "tooltip": "第 1 段文本。列表按 text1~text5 的顺序组织,\n" + "空白段会被自动跳过,不会在列表里留空位。" }), }, "optional": { "text2": ("STRING", { "multiline": True, - "default": "" + "default": "", + "tooltip": "第 2 段文本,留空则跳过。" }), "text3": ("STRING", { "multiline": True, - "default": "" + "default": "", + "tooltip": "第 3 段文本,留空则跳过。" }), "text4": ("STRING", { "multiline": True, - "default": "" + "default": "", + "tooltip": "第 4 段文本,留空则跳过。" }), "text5": ("STRING", { "multiline": True, - "default": "" + "default": "", + "tooltip": "第 5 段文本,留空则跳过。" }), } } diff --git a/utf8_converter_node.py b/utf8_converter_node.py index 3f9b5f6..e84ba32 100644 --- a/utf8_converter_node.py +++ b/utf8_converter_node.py @@ -80,7 +80,10 @@ class UTF8ConverterNode: "input_text": ("STRING", { "multiline": True, "default": "", - "placeholder": "输入文本 / Input Text" + "placeholder": "输入文本 / Input Text", + "tooltip": "待转码的文本。用于把从各处复制来的乱码文本\n" + "(GBK/UTF-16 等被误读的内容)修正为正常 UTF-8。\n" + "若下方填了文件路径,则以文件内容为准,本框会被忽略。" }), "remove_emoji": ("BOOLEAN", { "default": True, @@ -93,7 +96,10 @@ class UTF8ConverterNode: "input_file": ("STRING", { "multiline": False, "default": "", - "placeholder": "文件路径 / File Path (优先读取 / Overrides input_text)" + "placeholder": "文件路径 / File Path (优先读取 / Overrides input_text)", + "tooltip": "文本文件路径。填了它就优先读文件,忽略上面的输入框。\n" + "会依次尝试 UTF-8 → UTF-16(带BOM) → GB18030 解码,\n" + "都失败则忽略非法字节强行读出,尽量不让整个文件报废。" }), } } diff --git a/watermark_node.py b/watermark_node.py index 79da98e..5719e5c 100644 --- a/watermark_node.py +++ b/watermark_node.py @@ -89,28 +89,57 @@ class ImageWatermarkNode: else (font_labels[0] if font_labels else "") return { "required": { - "image": ("IMAGE",), + "image": ("IMAGE", { + "tooltip": "要打水印的图像,支持批量,每张都会独立处理。" + }), "text": ("STRING", { "default": "仅供参考 / SAMPLE", "multiline": True, + "tooltip": "水印文案,支持多行(直接在框里换行)。\n" + "多行会作为一个整体单元平铺,行内间距按字号的 25% 自动计算。" + }), + "font": (font_labels, { + "default": default_font, + "tooltip": "字体,列表来自 Ruinode/font 目录,与 Markdown 转图片节点共用。\n" + "放入新的 ttf/otf/ttc 后刷新页面即可出现。\n" + "列表里没有的字体会自动回退,不会报错。" }), - "font": (font_labels, {"default": default_font}), "font_size": ("INT", { "default": 48, "min": 8, "max": 500, "step": 1, + "tooltip": "文字大小(像素)。它同时是密度的基准——\n" + "改字号时水印疏密会等比缩放,视觉观感保持稳定。" }), "angle": ("FLOAT", { "default": 30.0, "min": -180.0, "max": 180.0, "step": 1.0, + "tooltip": "水印整体旋转角度(度)。\n" + "实现上先在超过对角线的画布上平铺、整体旋转后再从中心裁切,\n" + "所以任何角度下四角都不会露白。" }), "density": ("FLOAT", { "default": 1.0, "min": 0.1, "max": 5.0, "step": 0.05, + "tooltip": "水印密度,越大越密。控制的是水印之间的空隙:\n" + "空隙 = 字号 × 2 ÷ 密度。\n" + "0.4 左右 = 稀疏点缀;1.0 = 默认;\n" + "2.5 以上 = 密集防盗(建议同时把透明度降到 20~30)" }), "letter_spacing": ("INT", { "default": 0, "min": -20, "max": 200, "step": 1, + "tooltip": "字间距:单条文案内相邻字符的额外间距(像素)。\n" + "逐字符绘制实现,可为负值让字挤在一起。" }), "opacity": ("FLOAT", { "default": 35.0, "min": 0.0, "max": 100.0, "step": 1.0, + "tooltip": "水印透明程度:0 = 完全透明(原样返回,不做任何计算),\n" + "100 = 完全不透明。\n" + "防盗建议 25~40:看得清但不影响读图。" + }), + "color": ("STRING", { + "default": "#FFFFFF", "multiline": False, + "tooltip": "水印颜色,四种写法都支持:\n" + "#FFFFFF / #FFF / 255,255,255 / white\n" + "认不出的写法会回退成灰色,不会报错。\n" + "深色图用白色,浅色图用黑或深灰,彩色图用黄色最醒目。" }), - "color": ("STRING", {"default": "#FFFFFF", "multiline": False}), }, } diff --git a/zenmux/zenmux_node.py b/zenmux/zenmux_node.py index 4c9acc6..de5fab0 100644 --- a/zenmux/zenmux_node.py +++ b/zenmux/zenmux_node.py @@ -133,24 +133,35 @@ class ZenMuxNode: "api_key": ("STRING", { "default": "", "multiline": False, + "tooltip": "ZenMux 的 API Key。\n" + "⚠ 工作流会连同此值一起保存,分享 json 前记得清空。" }), # 全量模型标签(含价格),已按厂商排序聚类; # 下拉搜索框输入厂商前缀(如 "qwen/")即可过滤。 "model": (model_labels, { "default": default_label, + "tooltip": "模型,标签里直接带了输入/输出单价。\n" + "列表按厂商聚类排序——在下拉的搜索框输入厂商前缀\n" + "(如 qwen/ 、anthropic/ )即可快速过滤。\n" + "要传图请选支持视觉的型号,否则图会被忽略。" }), "system_prompt": ("STRING", { "default": "You are a helpful assistant.", "multiline": True, + "tooltip": "系统提示词:设定模型的角色与总体行为准则。\n" + "输出格式要求(如「只返回 JSON」)写在这里最稳定。" }), "user_prompt": ("STRING", { "default": "", "multiline": True, + "tooltip": "用户提示词:这一次具体要模型做什么。" }), "seed": ("INT", { "default": 0, "min": 0, "max": 0xffffffffffffffff, + "tooltip": "随机种子。多数模型并不真正支持复现,\n" + "这里主要用于强制节点重新执行(改了它就不会走缓存)。" }), }, "optional": { @@ -159,42 +170,63 @@ class ZenMuxNode: "min": 0.0, "max": 2.0, "step": 0.1, + "tooltip": "采样温度:越低越稳定保守,越高越发散。\n" + "结构化输出用 0~0.3,创意文案用 0.7~1.0。\n" + "部分新模型已弃用该参数,节点会自动重试并剔除它。" }), "top_p": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05, + "tooltip": "核采样:只在累计概率前 top_p 的词里挑。\n" + "与温度作用重叠,一般固定 1.0 只调温度,别两个一起动。" }), "max_tokens": ("INT", { "default": 1024, "min": 1, "max": 200000, + "tooltip": "回复的最大长度上限。设小了会把回答从中间截断。\n" + "注意它同时是费用上限的重要因素。" }), # 多模态图像输入(模型需支持 image 输入才有意义) - "image_1": ("IMAGE",), - "image_2": ("IMAGE",), - "image_3": ("IMAGE",), - "image_4": ("IMAGE",), - "image_5": ("IMAGE",), - "image_6": ("IMAGE",), + "image_1": ("IMAGE", { + "tooltip": "要一并发给模型的图像 1(需模型支持视觉)。\n" + "会按下方最大边长压缩后转 base64 提交。" + }), + "image_2": ("IMAGE", {"tooltip": "图像 2。"}), + "image_3": ("IMAGE", {"tooltip": "图像 3。"}), + "image_4": ("IMAGE", {"tooltip": "图像 4。"}), + "image_5": ("IMAGE", {"tooltip": "图像 5。"}), + "image_6": ("IMAGE", {"tooltip": "图像 6。图越多越贵、越慢。"}), "detail": (["auto", "low", "high"], { "default": "auto", + "tooltip": "图像细节级别:\n" + "low 便宜快速,只看大致内容;\n" + "high 切块细看,认小字/细节更准但更贵;\n" + "auto 由服务端决定。" }), "image_max_size": ("INT", { "default": 1024, "min": 256, "max": 4096, "step": 64, + "tooltip": "上传前把图缩放到的最大边长。\n" + "调小可显著省钱提速,但小字与细节会看不清。" }), # 高级:一般无需改动,留空即用官方地址(无需写 https://,会自动补全) "base_url": ("STRING", { "default": DEFAULT_BASE_URL_INPUT, "multiline": False, + "tooltip": "接口地址,一般不用改。\n" + "**不要写 https://** —— ComfyUI 前端会吞掉 \"://\",\n" + "协议由后端自动补全,这里只填域名和路径。" }), "proxy_url": ("STRING", { "default": "", "multiline": False, + "tooltip": "HTTP 代理,同样不要带协议前缀,只填 IP:端口,\n" + "例如 127.0.0.1:7890。留空表示直连。" }), # usage_stats 里人民币换算用的汇率,可按当日牌价自行调整 "usd_to_cny": ("FLOAT", { @@ -202,6 +234,9 @@ class ZenMuxNode: "min": 0.1, "max": 100.0, "step": 0.01, + "tooltip": "美元兑人民币汇率,仅用于把 usage_stats 输出里的\n" + "费用换算成人民币显示,不影响实际计费。\n" + "可按当日牌价自行调整。" }), }, }