diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 189d7d9bc..a4a85569b 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -326,11 +326,14 @@ class String(ComfyTypeIO): '''String input.''' def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, multiline=False, placeholder: str=None, default: str=None, dynamic_prompts: bool=None, + min_length: int=None, max_length: int=None, socketless: bool=None, force_input: bool=None, extra_dict=None, raw_link: bool=None, advanced: bool=None): super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, None, force_input, extra_dict, raw_link, advanced) self.multiline = multiline self.placeholder = placeholder self.dynamic_prompts = dynamic_prompts + self.min_length = min_length + self.max_length = max_length self.default: str def as_dict(self): @@ -338,6 +341,8 @@ class String(ComfyTypeIO): "multiline": self.multiline, "placeholder": self.placeholder, "dynamicPrompts": self.dynamic_prompts, + "minLength": self.min_length, + "maxLength": self.max_length, }) @comfytype(io_type="COMBO") diff --git a/execution.py b/execution.py index 75b021892..c54a64d89 100644 --- a/execution.py +++ b/execution.py @@ -215,6 +215,41 @@ def get_input_data(inputs, class_def, unique_id, execution_list=None, dynprompt= v3_data["hidden_inputs"] = hidden_inputs_v3 return input_data_all, missing_keys, v3_data +def validate_resolved_inputs(input_data_all, class_def, inputs): + """Validate resolved input values against schema constraints. + + This is needed because validate_inputs() only sees direct widget values. + Linked inputs aren't resolved during validate_inputs(), so this runs after resolution to catch any violations. + """ + is_v3 = issubclass(class_def, _ComfyNodeInternal) + valid_inputs = class_def.INPUT_TYPES() + if is_v3: + valid_inputs, _, _ = _io.get_finalized_class_inputs(valid_inputs, inputs) + + for x, values in input_data_all.items(): + input_type, input_category, extra_info = get_input_info(class_def, x, valid_inputs) + if extra_info is None: + continue + if input_type != "STRING": + continue + min_length = extra_info.get("minLength") + max_length = extra_info.get("maxLength") + if min_length is None and max_length is None: + continue + for val in values: + if val is None or not isinstance(val, str): + continue + if min_length is not None and len(val) < min_length: + raise ValueError( + f"Input '{x}': value length {len(val)} is shorter than " + f"minimum length of {min_length}" + ) + if max_length is not None and len(val) > max_length: + raise ValueError( + f"Input '{x}': value length {len(val)} is longer than " + f"maximum length of {max_length}" + ) + map_node_over_list = None #Don't hook this please async def resolve_map_node_over_list_results(results): @@ -498,6 +533,8 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed, execution_list.make_input_strong_link(unique_id, i) return (ExecutionResult.PENDING, None, None) + validate_resolved_inputs(input_data_all, class_def, inputs) + def execution_block_cb(block): if block.message is not None: mes = { @@ -938,6 +975,34 @@ async def validate_inputs(prompt_id, prompt, item, validated): errors.append(error) continue + if input_type == "STRING": + if "minLength" in extra_info and len(val) < extra_info["minLength"]: + error = { + "type": "value_shorter_than_min_length", + "message": "Value length {} shorter than min length of {}".format(len(val), extra_info["minLength"]), + "details": f"{x}", + "extra_info": { + "input_name": x, + "input_config": info, + "received_value": val, + } + } + errors.append(error) + continue + if "maxLength" in extra_info and len(val) > extra_info["maxLength"]: + error = { + "type": "value_longer_than_max_length", + "message": "Value length {} longer than max length of {}".format(len(val), extra_info["maxLength"]), + "details": f"{x}", + "extra_info": { + "input_name": x, + "input_config": info, + "received_value": val, + } + } + errors.append(error) + continue + if isinstance(input_type, list) or input_type == io.Combo.io_type: if input_type == io.Combo.io_type: combo_options = extra_info.get("options", [])