diff --git a/README.md b/README.md index f3dac63..0882e67 100644 --- a/README.md +++ b/README.md @@ -318,25 +318,17 @@ If you like what I share, please support me with [PayPal](https://paypal.me/jake 2. Add ABC and non-ABC version of Random Prompter Nodes switch to config.ini. 3. Add Random Prompter config to config.ini. 4. Add more choices for Random Prompter Nodes with more refined and accurate categories. + - 2025-11-07 - v2.3.1 1. Add Nodes description. + - 2025-11-08 - v2.3.2 1. Move SDXL Target res node back to misc from deprecated. + 2. Add more choice for Random Prompter. + - 2025-11-10 - v2.3.3 1. Fix path separator mismatch issue of Random Prompter in Linux. + 2. Add more choices for Random Prompter. + - 2025-11-10 - v2.3.4 1. Add System Prompter node for generating single image | shot script system prompt for LLM/VLM model. Supports JSON format and multi-language output request. QWen3-VL is recommended. + 2. Add Shot Script Extractor and Combiner nodes. + 3. Add QWen3-VL to auto-prompt workflows along with System Prompter node. -- 2025-11-07 - v2.3.1 -1. Add Nodes description. - -- 2025-11-08 - v2.3.2 -1. Move SDXL Target res node back to misc from deprecated. -2. Add more choice for Random Prompter. - -- 2025-11-10 - v2.3.3 -1. Fix path separator mismatch issue of Random Prompter in Linux. -2. Add more choices for Random Prompter. - -- 2025-11-10 - v2.3.4 -1. Add System Prompter node for generating single image | shot script system prompt for LLM/VLM model. Supports JSON format and multi-language output request. QWen3-VL is recommended. -2. Add Shot Script Extractor and Combiner nodes. -3. Add QWen3-VL to auto-prompt workflows along with System Prompter node. - - 2025-11-12 - v2.3.5 1. Update Shot Script Extractor and Combiner nodes. 2. Update Prompt Combiner node. @@ -348,9 +340,12 @@ If you like what I share, please support me with [PayPal](https://paypal.me/jake 2. Fix some typo issues. - 2025-11-15 - v2.3.7 -1. Add remove_prompt_emphasis function to Prompt Combiner. +1. Add remove_prompt_emphasis function to Random Prompter and Prompt Combiner. 2. Update Portrait Master + Random Prompter imgen workflow. +- 2025-11-17 - v2.3.8 +1. Update Shot Script Extractor node, supports extracting specific element from string list, dictionary stirng, array string or multiline string based on index or key value. + ## Installation 1. `git clone https://github.com/jakechai/ComfyUI-JakeUpgrade` into the `custom_nodes` folder - e.g. `custom_nodes\ComfyUI-JakeUpgrade` @@ -429,7 +424,7 @@ System Prompter JK🐉: Build single image | shot script system prompt for LLM/V ![image](imgs/nodes/Prompt/Shot%20Script%20Combiner%20JK.png) Shot Script Combiner JK🐉: Combine shot scripts into string list output. ![image](imgs/nodes/Prompt/Shot%20Script%20Extractor%20JK.png) -Shot Script Extractor JK🐉: Extract specific element from shot script or string list based on index. +Shot Script Extractor JK🐉: Extract specific element from string list, dictionary stirng, array string or multiline string based on index or key value. ![image](imgs/nodes/Prompt/Prompt%20Combine%20JK.png) Prompt Combine JK🐉: Merge the two strings into one and clean up the result. ![image](imgs/nodes/Prompt/promptworkflow.png) @@ -797,6 +792,7 @@ TriMesh Input Switch JK🐉 | CamCloneMaster [2.1](https://github.com/KwaiVGI/CamCloneMaster) | main model | t2v i2v | √ | | Video-as-prompt [2.1](https://bytedance.github.io/Video-As-Prompt/) | main & module model | i2v | √ | | Motion Stream [2.1](https://github.com/alex4727/motionstream) | not yet released | i2v | √ | +| Time-to-Move [2.2](https://time-to-move.github.io/) | main model | i2v | √ | ## JakeUpgrade Workflow diff --git a/imgs/nodes/Prompt/Shot Script Extractor JK.png b/imgs/nodes/Prompt/Shot Script Extractor JK.png index 383e445..205b065 100644 Binary files a/imgs/nodes/Prompt/Shot Script Extractor JK.png and b/imgs/nodes/Prompt/Shot Script Extractor JK.png differ diff --git a/imgs/nodes/Prompt/promptworkflow.png b/imgs/nodes/Prompt/promptworkflow.png index 4e531a4..bb8a153 100644 Binary files a/imgs/nodes/Prompt/promptworkflow.png and b/imgs/nodes/Prompt/promptworkflow.png differ diff --git a/nodes/jake_node_prompt.py b/nodes/jake_node_prompt.py index 0c6fdc6..21d2125 100644 --- a/nodes/jake_node_prompt.py +++ b/nodes/jake_node_prompt.py @@ -2240,6 +2240,33 @@ class ShotScriptUtils: return "", "" + @staticmethod + def extract_from_dict_by_key(data: dict, key: str) -> Tuple[str, str]: + """从字典中根据键名提取值""" + # 精确匹配 + if key in data: + return ShotScriptUtils.preserve_format(data[key]), key + + # 如果没有精确匹配,尝试大小写不敏感匹配 + key_lower = key.lower() + for dict_key, value in data.items(): + if str(dict_key).lower() == key_lower: + return ShotScriptUtils.preserve_format(value), dict_key + + # 如果没有找到,尝试将key转换为数字作为回退 + try: + index = int(key) + return ShotScriptUtils.extract_from_dict(data, index) + except (ValueError, TypeError): + pass + + return "", "" + + @staticmethod + def get_available_keys(data: dict) -> List[str]: + """获取字典中所有可用的键""" + return [str(key) for key in data.keys()] + @staticmethod def is_special_json_detail_format(data: dict) -> bool: """检查是否是特殊JSON格式(包含subject, background等字段)""" @@ -2418,143 +2445,180 @@ class ShotScriptExtractor_JK: def INPUT_TYPES(cls): return { "required": { - "shot_scripts": ("STRING", { + "string_inputs": ("STRING", { "default": "", "multiline": True, "tooltip": "Shot script JSON or string list to extract from" }), - "shot_index": ("INT", { - "default": 1, - "min": -999999, - "max": 999999, - "step": 1, - "tooltip": "Index of the element to extract (starting from 1, negative values count from the end)" + "key_or_index": ("STRING", { + "default": "1", + "tooltip": "Key name for dict_string, index for other types (supports negative indexing)" }), - "input_type": (["shot_script", "string_list"], { - "default": "shot_script", - "tooltip": "Input type: shot script or string list" + "input_type": (["string_list", "dict_string", "array_string", "multilines"], { + "default": "string_list", + "tooltip": "Input type: string_list, dict_string, array_string, or multilines" }), } } # 两个输入都是列表 INPUT_IS_LIST = True - RETURN_TYPES = ("STRING", "INT", "STRING") - RETURN_NAMES = ("shot_script", "shot_counts", "status") + RETURN_NAMES = ("element", "element_counts", "status") FUNCTION = "extract_element" CATEGORY = icons.get("JK/Prompt") - DESCRIPTION = "Extract specific element from shot script or string list based on index." + DESCRIPTION = "Extract specific element from string list, dictionary stirng, array string or multiline string based on index or key value." - def extract_element(self, shot_scripts, shot_index, input_type): - """Extract specific element from shot script or string list based on index""" + def extract_element(self, string_inputs, key_or_index, input_type): + """Extract specific element from shot script or string list based on key or index""" - if not shot_scripts: + if not string_inputs: return ("", 0, "Empty input") - # 获取索引值 - i = shot_index[0] + # 获取key_or_index值 + key_or_index_str = key_or_index[0] if key_or_index else "1" # 获取输入类型 - input_type = input_type[0] if input_type else "shot_script" + input_type = input_type[0] if input_type else "string_list" - if input_type == "shot_script": - # 使用原有的镜头脚本提取逻辑 - return self._extract_from_shot_script(shot_scripts[0], i) + if input_type == "dict_string": + # 使用字典键提取逻辑 + return self._extract_from_dict_string(string_inputs[0], key_or_index_str) + elif input_type == "array_string": + # 使用数组字符串提取逻辑 + return self._extract_from_array_string(string_inputs[0], key_or_index_str) + elif input_type == "multilines": + # 使用多行文本提取逻辑 + return self._extract_from_multilines(string_inputs[0], key_or_index_str) else: # 使用字符串列表提取逻辑 - return self._extract_from_string_list(shot_scripts, i) + return self._extract_from_string_list(string_inputs, key_or_index_str) - def _extract_from_shot_script(self, shot_script: str, shot_index: int) -> Tuple[str, int, str]: - """从镜头脚本中提取元素""" - if not shot_script.strip(): - return ("", 0, "Empty script") + def _extract_from_dict_string(self, dict_string: str, key: str) -> Tuple[str, int, str]: + """从字典字符串中根据键名提取元素""" + if not dict_string.strip(): + return ("", 0, "Empty dict string") try: - # 检测输入格式 - input_format = ShotScriptUtils.detect_format(shot_script) - shot_counts = 0 - result_script = "" # 使用不同的变量名避免冲突 - status = "" + # 解析JSON为字典 + data = json.loads(dict_string) - if input_format == "dict": - # JSON字典格式 - script_data = json.loads(shot_script) - - # 检查是否是特殊JSON格式(没有shot键) - if ShotScriptUtils.is_special_json_detail_format(script_data): - # 特殊格式:只有一个元素 - shot_counts = 1 - if shot_index == 1: - result_script = ShotScriptUtils.preserve_format(script_data) - status = "special_format" - else: - result_script = "" - status = f"Index out of range (1-{shot_counts})" - else: - # 普通字典格式 - shot_counts = ShotScriptUtils.count_shots_in_dict(script_data) - result_script, key = ShotScriptUtils.extract_from_dict(script_data, shot_index) - status = key if result_script else f"Index out of range (1-{shot_counts})" - - elif input_format == "list": - # JSON列表格式 - script_data = json.loads(shot_script) - shot_counts = len(script_data) - - # 处理负索引 - if shot_index < 0: - shot_index = shot_counts + shot_index + 1 - - if 1 <= shot_index <= shot_counts: - result_script = ShotScriptUtils.preserve_format(script_data[shot_index-1]) - status = f"item{shot_index}" - else: - result_script = "" - status = f"Index out of range (1-{shot_counts})" - + if not isinstance(data, dict): + return ("", 0, "Input is not a dictionary") + + # 获取所有可用键 + available_keys = ShotScriptUtils.get_available_keys(data) + shot_counts = len(available_keys) + + # 根据键名提取值 + result_script, matched_key = ShotScriptUtils.extract_from_dict_by_key(data, key) + + if result_script: + status = f"Key '{matched_key}' found" else: - # 字符串格式 - lines = ShotScriptUtils.split_string_into_lines(shot_script) - shot_counts = len(lines) - - # 处理负索引 - if shot_index < 0: - shot_index = shot_counts + shot_index + 1 - - if 1 <= shot_index <= shot_counts: - result_script = lines[shot_index-1] - status = f"line{shot_index}" - else: - result_script = "" - status = f"Index out of range (1-{shot_counts})" + status = f"Key '{key}' not found. Available keys: {', '.join(available_keys)}" return (result_script, shot_counts, status) + except json.JSONDecodeError as e: + return ("", 0, f"Invalid JSON: {str(e)}") except Exception as e: - print(f"Error extracting from shot script: {e}") return ("", 0, f"Error: {str(e)}") - def _extract_from_string_list(self, string_list: List[str], shot_index: int) -> Tuple[str, int, str]: + def _extract_from_array_string(self, array_string: str, index_str: str) -> Tuple[str, int, str]: + """从数组字符串中提取元素""" + if not array_string.strip(): + return ("", 0, "Empty array string") + + try: + # 解析JSON数组 + data = json.loads(array_string) + + if not isinstance(data, list): + return ("", 0, "Input is not an array") + + shot_counts = len(data) + + # 将索引字符串转换为整数 + try: + index = int(index_str) + except ValueError: + return ("", shot_counts, f"Invalid index: {index_str}") + + # 处理负索引 + if index < 0: + index = shot_counts + index + 1 + + # 检查索引是否有效 + if 1 <= index <= shot_counts: + result_script = ShotScriptUtils.preserve_format(data[index-1]) + status = f"Element {index} of {shot_counts}" + else: + result_script = "" + status = f"Index out of range (1-{shot_counts})" + + return (result_script, shot_counts, status) + + except json.JSONDecodeError as e: + return ("", 0, f"Invalid JSON: {str(e)}") + except Exception as e: + return ("", 0, f"Error: {str(e)}") + + def _extract_from_multilines(self, text: str, index_str: str) -> Tuple[str, int, str]: + """从多行文本中提取元素""" + if not text.strip(): + return ("", 0, "Empty text") + + # 分割文本为行 + lines = ShotScriptUtils.split_string_into_lines(text) + shot_counts = len(lines) + + # 将索引字符串转换为整数 + try: + index = int(index_str) + except ValueError: + return ("", shot_counts, f"Invalid index: {index_str}") + + # 处理负索引 + if index < 0: + index = shot_counts + index + 1 + + # 检查索引是否有效 + if 1 <= index <= shot_counts: + result_script = lines[index-1] + status = f"Line {index} of {shot_counts}" + else: + result_script = "" + status = f"Index out of range (1-{shot_counts})" + + return (result_script, shot_counts, status) + + def _extract_from_string_list(self, string_list: List[str], index_str: str) -> Tuple[str, int, str]: """从字符串列表中提取元素""" if not string_list: return ("", 0, "Empty list") shot_counts = len(string_list) + # 将索引字符串转换为整数 + try: + index = int(index_str) + except ValueError: + return ("", shot_counts, f"Invalid index: {index_str}") + # 处理负索引(从末尾开始计数) - if shot_index < 0: - shot_index = shot_counts + shot_index + 1 + if index < 0: + index = shot_counts + index + 1 # 检查索引是否有效 - if shot_index < 1 or shot_index > shot_counts: + if index < 1 or index > shot_counts: # 索引超出范围,返回最后一个元素 result_script = string_list[-1] status = f"Index out of range, returning last element ({shot_counts} of {shot_counts})" else: # 提取元素 - result_script = string_list[shot_index - 1] - status = f"Element {shot_index} of {shot_counts}" + result_script = string_list[index - 1] + status = f"Element {index} of {shot_counts}" return (result_script, shot_counts, status) diff --git a/nodes/jake_node_prompt_ABC.py b/nodes/jake_node_prompt_ABC.py index bb40a68..9cae253 100644 --- a/nodes/jake_node_prompt_ABC.py +++ b/nodes/jake_node_prompt_ABC.py @@ -2682,6 +2682,33 @@ class ShotScriptUtils: return "", "" + @staticmethod + def extract_from_dict_by_key(data: dict, key: str) -> Tuple[str, str]: + """从字典中根据键名提取值""" + # 精确匹配 + if key in data: + return ShotScriptUtils.preserve_format(data[key]), key + + # 如果没有精确匹配,尝试大小写不敏感匹配 + key_lower = key.lower() + for dict_key, value in data.items(): + if str(dict_key).lower() == key_lower: + return ShotScriptUtils.preserve_format(value), dict_key + + # 如果没有找到,尝试将key转换为数字作为回退 + try: + index = int(key) + return ShotScriptUtils.extract_from_dict(data, index) + except (ValueError, TypeError): + pass + + return "", "" + + @staticmethod + def get_available_keys(data: dict) -> List[str]: + """获取字典中所有可用的键""" + return [str(key) for key in data.keys()] + @staticmethod def is_special_json_detail_format(data: dict) -> bool: """检查是否是特殊JSON格式(包含subject, background等字段)""" @@ -2860,143 +2887,180 @@ class ShotScriptExtractor_JK: def INPUT_TYPES(cls): return { "required": { - "shot_scripts": ("STRING", { + "string_inputs": ("STRING", { "default": "", "multiline": True, "tooltip": "Shot script JSON or string list to extract from" }), - "shot_index": ("INT", { - "default": 1, - "min": -999999, - "max": 999999, - "step": 1, - "tooltip": "Index of the element to extract (starting from 1, negative values count from the end)" + "key_or_index": ("STRING", { + "default": "1", + "tooltip": "Key name for dict_string, index for other types (supports negative indexing)" }), - "input_type": (["shot_script", "string_list"], { - "default": "shot_script", - "tooltip": "Input type: shot script or string list" + "input_type": (["string_list", "dict_string", "array_string", "multilines"], { + "default": "string_list", + "tooltip": "Input type: string_list, dict_string, array_string, or multilines" }), } } # 两个输入都是列表 INPUT_IS_LIST = True - RETURN_TYPES = ("STRING", "INT", "STRING") - RETURN_NAMES = ("shot_script", "shot_counts", "status") + RETURN_NAMES = ("element", "element_counts", "status") FUNCTION = "extract_element" CATEGORY = icons.get("JK/Prompt") - DESCRIPTION = "Extract specific element from shot script or string list based on index." + DESCRIPTION = "Extract specific element from string list, dictionary stirng, array string or multiline string based on index or key value." - def extract_element(self, shot_scripts, shot_index, input_type): - """Extract specific element from shot script or string list based on index""" + def extract_element(self, string_inputs, key_or_index, input_type): + """Extract specific element from shot script or string list based on key or index""" - if not shot_scripts: + if not string_inputs: return ("", 0, "Empty input") - # 获取索引值 - i = shot_index[0] + # 获取key_or_index值 + key_or_index_str = key_or_index[0] if key_or_index else "1" # 获取输入类型 - input_type = input_type[0] if input_type else "shot_script" + input_type = input_type[0] if input_type else "string_list" - if input_type == "shot_script": - # 使用原有的镜头脚本提取逻辑 - return self._extract_from_shot_script(shot_scripts[0], i) + if input_type == "dict_string": + # 使用字典键提取逻辑 + return self._extract_from_dict_string(string_inputs[0], key_or_index_str) + elif input_type == "array_string": + # 使用数组字符串提取逻辑 + return self._extract_from_array_string(string_inputs[0], key_or_index_str) + elif input_type == "multilines": + # 使用多行文本提取逻辑 + return self._extract_from_multilines(string_inputs[0], key_or_index_str) else: # 使用字符串列表提取逻辑 - return self._extract_from_string_list(shot_scripts, i) + return self._extract_from_string_list(string_inputs, key_or_index_str) - def _extract_from_shot_script(self, shot_script: str, shot_index: int) -> Tuple[str, int, str]: - """从镜头脚本中提取元素""" - if not shot_script.strip(): - return ("", 0, "Empty script") + def _extract_from_dict_string(self, dict_string: str, key: str) -> Tuple[str, int, str]: + """从字典字符串中根据键名提取元素""" + if not dict_string.strip(): + return ("", 0, "Empty dict string") try: - # 检测输入格式 - input_format = ShotScriptUtils.detect_format(shot_script) - shot_counts = 0 - result_script = "" # 使用不同的变量名避免冲突 - status = "" + # 解析JSON为字典 + data = json.loads(dict_string) - if input_format == "dict": - # JSON字典格式 - script_data = json.loads(shot_script) - - # 检查是否是特殊JSON格式(没有shot键) - if ShotScriptUtils.is_special_json_detail_format(script_data): - # 特殊格式:只有一个元素 - shot_counts = 1 - if shot_index == 1: - result_script = ShotScriptUtils.preserve_format(script_data) - status = "special_format" - else: - result_script = "" - status = f"Index out of range (1-{shot_counts})" - else: - # 普通字典格式 - shot_counts = ShotScriptUtils.count_shots_in_dict(script_data) - result_script, key = ShotScriptUtils.extract_from_dict(script_data, shot_index) - status = key if result_script else f"Index out of range (1-{shot_counts})" - - elif input_format == "list": - # JSON列表格式 - script_data = json.loads(shot_script) - shot_counts = len(script_data) - - # 处理负索引 - if shot_index < 0: - shot_index = shot_counts + shot_index + 1 - - if 1 <= shot_index <= shot_counts: - result_script = ShotScriptUtils.preserve_format(script_data[shot_index-1]) - status = f"item{shot_index}" - else: - result_script = "" - status = f"Index out of range (1-{shot_counts})" - + if not isinstance(data, dict): + return ("", 0, "Input is not a dictionary") + + # 获取所有可用键 + available_keys = ShotScriptUtils.get_available_keys(data) + shot_counts = len(available_keys) + + # 根据键名提取值 + result_script, matched_key = ShotScriptUtils.extract_from_dict_by_key(data, key) + + if result_script: + status = f"Key '{matched_key}' found" else: - # 字符串格式 - lines = ShotScriptUtils.split_string_into_lines(shot_script) - shot_counts = len(lines) - - # 处理负索引 - if shot_index < 0: - shot_index = shot_counts + shot_index + 1 - - if 1 <= shot_index <= shot_counts: - result_script = lines[shot_index-1] - status = f"line{shot_index}" - else: - result_script = "" - status = f"Index out of range (1-{shot_counts})" + status = f"Key '{key}' not found. Available keys: {', '.join(available_keys)}" return (result_script, shot_counts, status) + except json.JSONDecodeError as e: + return ("", 0, f"Invalid JSON: {str(e)}") except Exception as e: - print(f"Error extracting from shot script: {e}") return ("", 0, f"Error: {str(e)}") - def _extract_from_string_list(self, string_list: List[str], shot_index: int) -> Tuple[str, int, str]: + def _extract_from_array_string(self, array_string: str, index_str: str) -> Tuple[str, int, str]: + """从数组字符串中提取元素""" + if not array_string.strip(): + return ("", 0, "Empty array string") + + try: + # 解析JSON数组 + data = json.loads(array_string) + + if not isinstance(data, list): + return ("", 0, "Input is not an array") + + shot_counts = len(data) + + # 将索引字符串转换为整数 + try: + index = int(index_str) + except ValueError: + return ("", shot_counts, f"Invalid index: {index_str}") + + # 处理负索引 + if index < 0: + index = shot_counts + index + 1 + + # 检查索引是否有效 + if 1 <= index <= shot_counts: + result_script = ShotScriptUtils.preserve_format(data[index-1]) + status = f"Element {index} of {shot_counts}" + else: + result_script = "" + status = f"Index out of range (1-{shot_counts})" + + return (result_script, shot_counts, status) + + except json.JSONDecodeError as e: + return ("", 0, f"Invalid JSON: {str(e)}") + except Exception as e: + return ("", 0, f"Error: {str(e)}") + + def _extract_from_multilines(self, text: str, index_str: str) -> Tuple[str, int, str]: + """从多行文本中提取元素""" + if not text.strip(): + return ("", 0, "Empty text") + + # 分割文本为行 + lines = ShotScriptUtils.split_string_into_lines(text) + shot_counts = len(lines) + + # 将索引字符串转换为整数 + try: + index = int(index_str) + except ValueError: + return ("", shot_counts, f"Invalid index: {index_str}") + + # 处理负索引 + if index < 0: + index = shot_counts + index + 1 + + # 检查索引是否有效 + if 1 <= index <= shot_counts: + result_script = lines[index-1] + status = f"Line {index} of {shot_counts}" + else: + result_script = "" + status = f"Index out of range (1-{shot_counts})" + + return (result_script, shot_counts, status) + + def _extract_from_string_list(self, string_list: List[str], index_str: str) -> Tuple[str, int, str]: """从字符串列表中提取元素""" if not string_list: return ("", 0, "Empty list") shot_counts = len(string_list) + # 将索引字符串转换为整数 + try: + index = int(index_str) + except ValueError: + return ("", shot_counts, f"Invalid index: {index_str}") + # 处理负索引(从末尾开始计数) - if shot_index < 0: - shot_index = shot_counts + shot_index + 1 + if index < 0: + index = shot_counts + index + 1 # 检查索引是否有效 - if shot_index < 1 or shot_index > shot_counts: + if index < 1 or index > shot_counts: # 索引超出范围,返回最后一个元素 result_script = string_list[-1] status = f"Index out of range, returning last element ({shot_counts} of {shot_counts})" else: # 提取元素 - result_script = string_list[shot_index - 1] - status = f"Element {shot_index} of {shot_counts}" + result_script = string_list[index - 1] + status = f"Element {index} of {shot_counts}" return (result_script, shot_counts, status) diff --git a/pyproject.toml b/pyproject.toml index 22ceb03..d46dd7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-jakeupgrade" description = "ComfyUI workflow customization by Jake." -version = "2.3.7" +version = "2.3.8" license = {file = "LICENSE"} dependencies = ["opencv-python", "piexif", "torch", "torchvision", "numpy", "pillow", "simpleeval", "functools", "PyYAML", "toml"]