1. Update System Prompter node, supports shot scripts for video generation.
2. Update workflows including System Prompter node.
This commit is contained in:
jakechai
2026-02-15 13:29:55 +08:00
parent 7ff93c5347
commit e6d768af54
23 changed files with 98 additions and 232 deletions
+4 -1
View File
@@ -412,6 +412,10 @@ If you like what I share, please support me with [PayPal](https://paypal.me/jake
1. Update the JK_module_Imgen_Seamless Texture workflow and remove deprecated nodes.
2. Update the JK_workflow_imgen_PortraitMaster+RandomPrompter workflow and using the latest Portrait Master nodes.
- 2026-02-15 - v2.6.1
1. Update System Prompter node, supports shot scripts for video generation.
2. Update workflows including System Prompter node.
## Installation
1. `git clone https://github.com/jakechai/ComfyUI-JakeUpgrade` into the `custom_nodes` folder
- e.g. `custom_nodes\ComfyUI-JakeUpgrade`
@@ -432,7 +436,6 @@ If you like what I share, please support me with [PayPal](https://paypal.me/jake
- [JakeUpgrade Subgraphs](replacement/subgraphs): Copy all the subgraph JSON files into `...\ComfyUI\user\default\subgraphs`.
- [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) : Copy `_JK.pack` or `__JK_1_15_13.pack`(for `comfyui-frontend-package<=1.15.13`) to `...\ComfyUI\user\default\ComfyUI-Manager\components` for saving all JK Group Nodes within each workflow file.
- [IP Adapter Plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) : (Workaround before IPAdapter approves my pull request) Copy and replace files to `custom_nodes\ComfyUI_IPAdapter_plus` for better API workflow control by adding "None" selection.
- [Seamless tiling](https://github.com/spinagon/ComfyUI-seamless-tiling) : Provide the modified `SeamlessTile.py` for seamless-tiling workflow since it does not work with the latest ComfyUI and is inactive.
> [!NOTE]
> - Subraphs can be used as custom nodes if you copy the JSON files to the `...\ComfyUI\user\default\subgraphs`.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+38 -15
View File
@@ -65,8 +65,12 @@ class SysPromptBuilder:
text = text.replace("<shot_count>", str(shot_count))
# Replace <json_detail_format>
json_detail_format_str = self.preset_data.get("json_detail", {}).get("format", "{}")
json_detail_format_str = self.preset_data.get("json_detail", {}).get("for_image", {}).get("format", "{}")
json_detail_format_global_str = self.preset_data.get("json_detail", {}).get("for_video", {}).get("format_global", "{}")
json_detail_format_shot_str = self.preset_data.get("json_detail", {}).get("for_video", {}).get("format_shot", "{}")
text = text.replace("<json_detail_format>", json_detail_format_str)
text = text.replace("<json_detail_format_global>", json_detail_format_global_str)
text = text.replace("<json_detail_format_shot>", json_detail_format_shot_str)
return text
@@ -91,8 +95,8 @@ class SysPromptBuilder:
except Exception as e:
return ""
def _build_common_parts(self, model_type: str, mode: str, detail: str, json_detail_format: bool,
input_as_1st_shot: bool) -> list:
def _build_common_parts(self, model_type: str, mode: str, shot_continuity: bool, shot_script_for: bool,
detail: str, json_detail_format: bool, input_as_1st_shot: bool) -> list:
"""Build common template parts for both LLM and VLM"""
json_key = "json_detail" if json_detail_format else "no_json_detail"
@@ -103,7 +107,7 @@ class SysPromptBuilder:
["detail_preset", detail],
[model_type, "single", "part_02"],
["add_json_detail", "single", json_key],
["json_detail", "description"]
["json_detail", "for_image", "description"]
]
else:
return [
@@ -123,12 +127,16 @@ class SysPromptBuilder:
[model_type, "script", "part_03"],
(["first_shot_02"] if input_as_1st_shot else []),
["detail_preset", detail],
(["shot_continuity"] if shot_continuity else []),
[model_type, "script", "part_04"],
(["first_shot_03"] if input_as_1st_shot else []),
["add_json_detail", "script", json_key],
["json_detail", "description"],
["json_detail", "common"],
(["json_detail", "for_image", "description"] if (not shot_script_for) or (not shot_continuity and shot_script_for) else []),
(["json_detail", "for_video", "description_shot"] if shot_continuity and shot_script_for else []),
[model_type, "script", "part_05"],
["combine_shots"]
["combine_shots"],
(["json_detail", "for_video", "description_global"] if shot_continuity and shot_script_for else []),
]
else:
return [
@@ -140,14 +148,17 @@ class SysPromptBuilder:
[model_type, "script", "part_03"],
(["first_shot_02"] if input_as_1st_shot else []),
["detail_preset", detail],
(["shot_continuity"] if shot_continuity else []),
[model_type, "script", "part_04"],
(["first_shot_03"] if input_as_1st_shot else []),
["add_json_detail", "script", json_key],
(["video_common_description_no_json_detail", "shot"] if shot_continuity and shot_script_for else []),
[model_type, "script", "part_05"],
["combine_shots"]
["combine_shots"],
(["video_common_description_no_json_detail", "global"] if shot_continuity and shot_script_for else []),
]
def build_prompt(self, model: str, mode: str, detail: str, json_detail_format: bool,
def build_prompt(self, model: str, mode: str, shot_continuity: bool, shot_script_for: bool, detail: str, json_detail_format: bool,
shot_count: int, input_as_1st_shot: bool, system_language: str, output_language: str) -> str:
"""Build prompt based on input parameters"""
@@ -155,7 +166,7 @@ class SysPromptBuilder:
model_type = model.lower()
# Build template parts based on model and mode
template_parts = self._build_common_parts(model_type, mode, detail, json_detail_format, input_as_1st_shot)
template_parts = self._build_common_parts(model_type, mode, shot_continuity, shot_script_for, detail, json_detail_format, input_as_1st_shot)
# Add final part for LLM
if model == "Text":
@@ -190,13 +201,15 @@ class SystemPrompter_JK:
"default": "single image",
"tooltip": "Select mode: single image or shot script."
}),
"detail": (["simple", "detailed", "extreme_detailed"], {
"default": "detailed",
"tooltip": "Select detail level for prompt generation."
"shot_continuity": ("BOOLEAN", {
"default": True,
"tooltip": "Shot script continuity setting."
}),
"json_detail_format": ("BOOLEAN", {
"shot_script_for": ("BOOLEAN", {
"default": False,
"tooltip": "Only available for QWen3-VL for now. Whether to output in JSON format with detailed breakdown."
"label_off": "image",
"label_on": "video",
"tooltip": "Shot script for image generation or video generation."
}),
"shot_count": ("INT", {
"default": 3,
@@ -209,6 +222,14 @@ class SystemPrompter_JK:
"default": False,
"tooltip": "Whether to use the custom prompt or reference image as the first shot"
}),
"detail": (["simple", "detailed", "extreme_detailed"], {
"default": "detailed",
"tooltip": "Select detail level for prompt generation."
}),
"json_detail_format": ("BOOLEAN", {
"default": False,
"tooltip": "Only available for QWen3-VL for now. Whether to output in JSON format with detailed breakdown."
}),
"system_language": (["Chinese", "English"], {
"default": "English",
"tooltip": "System language for LLM/VLM."
@@ -231,13 +252,15 @@ class SystemPrompter_JK:
def __init__(self):
self.builder = SysPromptBuilder()
def build_prompt(self, model: str, mode: str, detail: str, json_detail_format: bool,
def build_prompt(self, model: str, mode: str, shot_continuity: bool, shot_script_for: bool, detail: str, json_detail_format: bool,
shot_count: int, input_as_1st_shot: bool, system_language: str, output_language: str) -> Tuple[str]:
"""Build and return the prompt string"""
prompt = self.builder.build_prompt(
model=model,
mode=mode,
shot_continuity=shot_continuity,
shot_script_for=shot_script_for,
detail=detail,
json_detail_format=json_detail_format,
shot_count=shot_count,
+41 -9
View File
@@ -86,11 +86,29 @@
}
},
"json_detail": {
"description": {
"en": "The detailed descriptions include the subject, background(scene, season, time, weather), atmosphere(mood and color tone), action, expression, lighting, camera type (shot size and view angle), camera movement, and style. Place the detailed descriptions into the corresponding areas of a JSON file and output them in <specified_language> language, <json_detail_format>. \n",
"cn": "细节描述包含主体、背景(场景、季节、时间、天气)、氛围(气氛及色调)、动作、表情、灯光、相机类型(景别及视角)、相机运动、风格。将细节描述放入JSON格式中对应区域并以<特定语种>输出,<json_detail_format>。\n"
"common": {
"en": "The detailed descriptions include the subject, background(scene, season, time, weather), atmosphere(mood and color tone), action, expression, lighting, camera type (shot size and view angle), camera movement, and style. ",
"cn": "细节描述包含主体、背景(场景、季节、时间、天气)、氛围(气氛及色调)、动作、表情、灯光、相机类型(景别及视角)、相机运动、风格。"
},
"format": "{'subject': '', 'background': '', 'atmosphere': '', 'motion': '', 'expression': '', 'lighting': '', 'shot type': '', 'movement': '', 'style': ''}"
"for_image": {
"description": {
"en": "Place the detailed descriptions into the corresponding areas of a JSON file and output them in <specified_language> language, <json_detail_format>; 'motion' refers to the action of the subject, and 'movement' refers to the movement of the camera; \n",
"cn": "将细节描述放入JSON格式中对应区域并以<特定语种>输出,<json_detail_format>;'motion'指主体动作,'movement'指相机运动;\n"
},
"format": "{'subject': '', 'background': '', 'atmosphere': '', 'motion': '', 'expression': '', 'lighting': '', 'shot type': '', 'movement': '', 'style': ''}"
},
"for_video": {
"description_global": {
"en": "Add a 'Global' item at the beginning of the JSON format, place the subject, background(scene, season, time, weather), atmosphere(mood and color tone), action, expression, lighting, camera type (shot size and view angle), camera movement, and style detailed descriptions of all shot scripts in the corresponding area and output them in <specified_language>, <json_detail_format_global>; \n",
"cn": "在JSON格式最前面增加'Global'项,将主体、背景(场景、季节、时间、天气)、氛围(气氛及色调)、灯光、风格细节描述放入对应区域并以<特定语种>输出,<json_detail_format_global>;\n"
},
"description_shot": {
"en": "Place the action, expression, lighting, camera type (shot size and view angle), camera movement detailed descriptions into the corresponding areas of a JSON file and output them in <specified_language> language, <json_detail_format_shot>; 'motion' refers to the action of the subject, and 'movement' refers to the movement of the camera; \n",
"cn": "将各分镜头的动作、表情、相机类型(景别及视角)、相机运动细节描述放入JSON格式中对应区域并以<特定语种>输出,<json_detail_format_shot>;'motion'指主体动作,'movement'指相机运动;\n"
},
"format_global": "{'subject': '', 'background': '', 'atmosphere': '', 'lighting': '', 'style': ''}",
"format_shot": "{'motion': '', 'expression': '', 'shot type': '', 'movement': ''}"
}
},
"add_json_detail": {
"single": {
@@ -115,15 +133,29 @@
}
},
"split_shots": {
"en": "Create <shot_count> shot scripts based on the story's development, ensuring they align with the previous intent; follow the story and content sequence structure described in it, and ensure that the information hierarchy is clear, the continuity of elements between shots, and the connection of camera framing, perspective and movement; \n",
"cn": "根据故事发展,创作<shot_count>个分镜头,必须和之前的意图一致;遵循其中描述的故事及内容顺序结构,确保信息的层次清晰、前后镜头之间各元素的延续以及相机的景别、视角和运动的衔接;\n"
"en": "Create <shot_count> shot scripts based on the story's development; Follow the story and content sequence structure described in it, and ensure that the information hierarchy is clear, the continuity of elements between shots, and the connection of camera framing, perspective and movement; \n",
"cn": "根据故事发展,创作<shot_count>个分镜头;遵循故事及内容顺序结构,确保信息的层次清晰、前后镜头之间各元素的延续以及相机的景别、视角和运动的衔接;\n"
},
"shot_continuity": {
"en": " Descriptions that mean 'the same', 'identical', 'unchanged' or 'still' as the previous description are strictly prohibited. It is also strictly prohibited to use subjects (such as characters), backgrounds (such as scenes) or visual styles that are different from the previous description. Instead, the subjects (such as characters), backgrounds (such as scenes) or visual styles descriptions must be repeated, but appropriate modifications and additions of details are allowed as needed for the storyboard; ",
"cn": "严禁使用意为与上述“同样”、“同一”、“不变”或“依旧”含义的描述,严禁使用不同的主体(如人物)、背景(如场景)或画面风格,而必须重复使用主体(如人物)、背景(如场景)或画面风格,但允许根据分镜头需要适当修改并添加细节;"
},
"combine_shots": {
"en": "Combine all shot scripts into a JSON format with keys representing shot numbers. \n",
"cn": "将所有分镜头脚本放在按JSON文件代表对应镜头编号的英文名称简写中。\n"
"en": "Combine all shot scripts into a JSON format with keys representing shot numbers(Prefixed with 'shot_'). \n",
"cn": "将所有分镜头脚本放在按JSON格式代表对应镜头编号的英文名称简写中(以'shot'_为前缀)。\n"
},
"video_common_description_no_json_detail": {
"global": {
"en": "Add a 'Global' item at the beginning of the JSON format, redescribe the subject (such as characters), background (such as scenes), lighting and visual style in one sentence and output in <specified_language>. \n",
"cn": "在JSON格式最前面增加'Global'项,将所有分镜头脚本中的主体(如人物)、背景(如场景)、灯光及画面风格等内容,重新描述成一句话并按<特定语种>输出。\n"
},
"shot": {
"en": "Redescribes motions, expressions, camera type (shot size and angle), and camera movement and output them in <specified_language> for Each shot script; \n",
"cn": "将每个分镜脚本中的动作、表情、相机类型(景别及视角)、相机运动等内容,重新描述并以<特定语种>输出;\n"
}
},
"first_shot_01": {
"en": "And use this prompt as the first shot script;",
"en": " And use this prompt as the first shot script;",
"cn": "并将这个提示词作为第一个分镜头脚本;"
},
"first_shot_02": {
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-jakeupgrade"
description = "ComfyUI workflow customization by Jake."
version = "2.6.0"
version = "2.6.1"
license = {file = "LICENSE"}
dependencies = ["opencv-python", "piexif", "torch", "torchvision", "numpy", "pillow", "simpleeval", "PyYAML", "toml"]
@@ -1,192 +0,0 @@
from typing import Optional
import PIL
import torch
from torch import Tensor
from torch.nn import Conv2d
from torch.nn import functional as F
from torch.nn.modules.utils import _pair
class SeamlessTile:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"tiling": (["enable", "x_only", "y_only", "disable"],),
"copy_model": (["Modify in place"],),
},
}
CATEGORY = "conditioning"
RETURN_TYPES = ("MODEL",)
FUNCTION = "run"
def run(self, model, copy_model, tiling):
# 直接修改原始模型,不复制
if tiling == "enable":
make_circular_asymm(model.model, True, True)
elif tiling == "x_only":
make_circular_asymm(model.model, True, False)
elif tiling == "y_only":
make_circular_asymm(model.model, False, True)
else:
make_circular_asymm(model.model, False, False)
return (model,)
# asymmetric tiling from https://github.com/tjm35/asymmetric-tiling-sd-webui/blob/main/scripts/asymmetric_tiling.py
def make_circular_asymm(model, tileX: bool, tileY: bool):
# 记录已经修改过的层,避免重复修改
modified_layers = set()
for layer in model.modules():
if isinstance(layer, torch.nn.Conv2d) and id(layer) not in modified_layers:
# 标记为已修改
modified_layers.add(id(layer))
# 保存原始状态(如果需要恢复)
if not hasattr(layer, '_original_padding_mode'):
layer._original_padding_mode = layer.padding_mode
# 设置新的padding模式
layer.padding_modeX = 'circular' if tileX else 'constant'
layer.padding_modeY = 'circular' if tileY else 'constant'
# 获取padding值
# 注意:_reversed_padding_repeated_twice是(左, 右, 上, 下)
padding = layer._reversed_padding_repeated_twice
layer.paddingX = (padding[0], padding[1], 0, 0)
layer.paddingY = (0, 0, padding[2], padding[3])
# 保存原始_conv_forward
if not hasattr(layer, '_original_conv_forward'):
layer._original_conv_forward = layer._conv_forward
# 替换_conv_forward
layer._conv_forward = __replacementConv2DConvForward.__get__(layer, Conv2d)
return model
def __replacementConv2DConvForward(self, input: Tensor, weight: Tensor, bias: Optional[Tensor]):
# 应用X方向填充
working = F.pad(input, self.paddingX, mode=self.padding_modeX)
# 应用Y方向填充
working = F.pad(working, self.paddingY, mode=self.padding_modeY)
# 执行卷积(padding设为0,因为我们已经手动应用了填充)
return F.conv2d(working, weight, bias, self.stride, _pair(0), self.dilation, self.groups)
class CircularVAEDecode:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"samples": ("LATENT",),
"vae": ("VAE",),
"tiling": (["enable", "x_only", "y_only", "disable"],),
"copy_vae": (["Modify in place"],),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "decode"
CATEGORY = "latent"
def decode(self, samples, vae, tiling, copy_vae):
# 保存原始状态
original_states = {}
# 收集原始状态
for name, module in vae.first_stage_model.named_modules():
if isinstance(module, torch.nn.Conv2d):
state = {}
# 保存原始属性
for attr in ['padding_modeX', 'padding_modeY', 'paddingX', 'paddingY', '_conv_forward']:
if hasattr(module, attr):
state[attr] = getattr(module, attr)
original_states[name] = state
try:
# 临时修改VAE
if tiling == "enable":
make_circular_asymm(vae.first_stage_model, True, True)
elif tiling == "x_only":
make_circular_asymm(vae.first_stage_model, True, False)
elif tiling == "y_only":
make_circular_asymm(vae.first_stage_model, False, True)
else:
make_circular_asymm(vae.first_stage_model, False, False)
# 解码
result = vae.decode(samples["samples"])
finally:
# 恢复原始状态
for name, module in vae.first_stage_model.named_modules():
if name in original_states:
state = original_states[name]
for attr, value in state.items():
setattr(module, attr, value)
return (result,)
class MakeCircularVAE:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"vae": ("VAE",),
"tiling": (["enable", "x_only", "y_only", "disable"],),
"copy_vae": (["Modify in place"],),
}
}
RETURN_TYPES = ("VAE",)
FUNCTION = "run"
CATEGORY = "latent"
def run(self, vae, tiling, copy_vae):
# 直接修改原始VAE
if tiling == "enable":
make_circular_asymm(vae.first_stage_model, True, True)
elif tiling == "x_only":
make_circular_asymm(vae.first_stage_model, True, False)
elif tiling == "y_only":
make_circular_asymm(vae.first_stage_model, False, True)
else:
make_circular_asymm(vae.first_stage_model, False, False)
return (vae,)
class OffsetImage:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pixels": ("IMAGE",),
"x_percent": (
"FLOAT",
{"default": 50.0, "min": 0.0, "max": 100.0, "step": 1},
),
"y_percent": (
"FLOAT",
{"default": 50.0, "min": 0.0, "max": 100.0, "step": 1},
),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "image"
def run(self, pixels, x_percent, y_percent):
n, y, x, c = pixels.size()
y = round(y * y_percent / 100)
x = round(x * x_percent / 100)
return (pixels.roll((y, x), (1, 2)),)