diff --git a/README.md b/README.md index 81ba4ec..1171749 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,10 @@ When processing video frames through ComfyUI, face detection bboxes can jitter f ## Changelog +### v2.1.3 +- **FIX**: `VALIDATE_INPUTS` now includes required combo fields (`aspect_ratio`, `output_mode`, `classifier_type`) in its signature, forcing ComfyUI to delegate their validation to our method instead of doing strict framework-level "Value not in list" checks. Legacy workflows with misaligned positional `widgets_values` (e.g. `'auto'` landing on `output_mode`, `0` landing on `classifier_type`) are now caught and replaced with defaults. +- **FIX**: Added defensive combo sanitization in both v1 and v3 execute methods — invalid combo values are replaced with defaults at runtime as a safety net. + ### v2.1.2 - **FIX**: `instance_id` input type changed from INT to STRING. Legacy workflows pass `instance_id="default"` (string) from old v1 nodes — ComfyUI's `validate_inputs` runs before `execute()`, so `_coerce_int` never fires. Since `instance_id` is only a dict key, STRING is the correct type. diff --git a/face_detection_node.py b/face_detection_node.py index 74aa4a3..51db476 100644 --- a/face_detection_node.py +++ b/face_detection_node.py @@ -1,7 +1,20 @@ """ FaceDetectionNode — Optimized for H100 Cloud & LTX-Video Avatar Pipelines ======================================================================== -Version: 2.1.2 +Version: 2.1.3 + +CHANGELOG from v2.1.2 → v2.1.3: + 1. FIX: VALIDATE_INPUTS now includes required combo fields (aspect_ratio, + output_mode, classifier_type) in its signature. This causes ComfyUI to + delegate their validation to our method instead of doing strict + framework-level "Value not in list" checks. Legacy workflows with + misaligned positional widgets_values (e.g. 'auto' landing on output_mode, + 0 landing on classifier_type) are now caught and replaced with defaults. + 2. Added _VALID_OUTPUT_MODES, _VALID_CLASSIFIER_TYPES, _VALID_ASPECT_RATIOS + tuples and their defaults for consistent validation across VALIDATE_INPUTS + and both v1/v3 execute methods. + 3. Added defensive combo sanitization in v1 and v3 execute methods — invalid + values are replaced with defaults at runtime as a safety net. CHANGELOG from v2.1.1 → v2.1.2: 1. FIX: instance_id input type changed from INT to STRING. Legacy workflows @@ -95,6 +108,12 @@ ASPECT_RATIOS: dict[str, Optional[float]] = { # ────────────────────────────────────────────────────────────────────────────── _VALID_FACE_OUTPUT_FORMATS = ("strip", "individual") _DEFAULT_FACE_OUTPUT_FORMAT = "strip" +_VALID_OUTPUT_MODES = ("largest_face", "all_faces") +_DEFAULT_OUTPUT_MODE = "largest_face" +_VALID_CLASSIFIER_TYPES = ("default", "alternative") +_DEFAULT_CLASSIFIER_TYPE = "default" +_VALID_ASPECT_RATIOS = ("auto", "1:1", "9:16", "16:9", "4:3") +_DEFAULT_ASPECT_RATIO = "auto" # Default values for INT inputs — used by _coerce_int when legacy workflows # pass string values (e.g. "default" from classifier_type) into INT slots. @@ -468,6 +487,21 @@ if COMFY_V3: instance_id = str(instance_id) if instance_id is not None else "0" padding = _coerce_int(padding, "padding", 0) + # Defensive: sanitize combo fields that may have wrong values from + # legacy workflows with misaligned positional widgets_values + if aspect_ratio not in _VALID_ASPECT_RATIOS: + logger.warning("FaceDetectionNode: aspect_ratio='%s' invalid, using '%s'", + aspect_ratio, _DEFAULT_ASPECT_RATIO) + aspect_ratio = _DEFAULT_ASPECT_RATIO + if output_mode not in _VALID_OUTPUT_MODES: + logger.warning("FaceDetectionNode: output_mode='%s' invalid, using '%s'", + output_mode, _DEFAULT_OUTPUT_MODE) + output_mode = _DEFAULT_OUTPUT_MODE + if classifier_type not in _VALID_CLASSIFIER_TYPES: + logger.warning("FaceDetectionNode: classifier_type='%s' invalid, using '%s'", + classifier_type, _DEFAULT_CLASSIFIER_TYPE) + classifier_type = _DEFAULT_CLASSIFIER_TYPE + target_ratio = ASPECT_RATIOS.get(aspect_ratio, None) # Resolve padding: legacy padding (px) overrides auto_padding_ratio if > 0 @@ -676,13 +710,13 @@ class FaceDetectionNodeV1: "default": 35, "min": 0, "max": 100, "tooltip": "Padding as % of detected face size", }), - "aspect_ratio": ([["auto", "1:1", "9:16", "16:9", "4:3"]], { + "aspect_ratio": (["auto", "1:1", "9:16", "16:9", "4:3"], { "default": "auto", }), - "output_mode": ([["largest_face", "all_faces"]], { + "output_mode": (["largest_face", "all_faces"], { "default": "largest_face", }), - "classifier_type": ([["default", "alternative"]], { + "classifier_type": (["default", "alternative"], { "default": "default", }), }, @@ -723,16 +757,40 @@ class FaceDetectionNodeV1: DESCRIPTION = "Face Detection v2 — Auto-Padding, Temporal Smoothing, Aspect Ratios, Batch Processing" @classmethod - def VALIDATE_INPUTS(cls, face_output_format=None, padding=None, - temporal_smoothing=None, output_height=None, - instance_id=None, **kwargs): - """Validate optional inputs — gracefully handle type mismatches. + def VALIDATE_INPUTS(cls, aspect_ratio=None, output_mode=None, + classifier_type=None, face_output_format=None, + padding=None, temporal_smoothing=None, + output_height=None, instance_id=None, **kwargs): + """Validate all combo and optional inputs — gracefully handle type mismatches. When ComfyUI loads an old workflow, widgets_values are mapped positionally. - This can cause string values (e.g. 'default' from classifier_type) to land - on INT-typed inputs. We coerce or fall back to defaults here so the - framework validation doesn't crash. + This can cause string values (e.g. 'auto' from aspect_ratio) to land on + output_mode, or 'largest_face' to land on face_output_format. We validate + and replace invalid values with defaults so the framework doesn't crash. + + By including required combo fields (aspect_ratio, output_mode, + classifier_type) in this method signature, ComfyUI delegates their + validation to us instead of doing strict framework-level checking. """ + # Validate required combo fields (may receive wrong values from + # positional widget mapping in legacy workflows) + for name, val, valid_set, default in [ + ("aspect_ratio", aspect_ratio, + _VALID_ASPECT_RATIOS, _DEFAULT_ASPECT_RATIO), + ("output_mode", output_mode, + _VALID_OUTPUT_MODES, _DEFAULT_OUTPUT_MODE), + ("classifier_type", classifier_type, + _VALID_CLASSIFIER_TYPES, _DEFAULT_CLASSIFIER_TYPE), + ("face_output_format", face_output_format, + _VALID_FACE_OUTPUT_FORMATS, _DEFAULT_FACE_OUTPUT_FORMAT), + ]: + if val is not None and val not in valid_set: + logger.warning( + "FaceDetectionNode: invalid %s='%s', " + "using '%s' instead. Valid: %s", + name, val, default, list(valid_set), + ) + # Coerce INT inputs that may arrive as strings from positional widget mapping # Note: instance_id is now STRING — no longer needs INT coercion for name, val in [ @@ -743,14 +801,6 @@ class FaceDetectionNodeV1: if val is not None and not isinstance(val, int): _coerce_int(val, name) # logs warning on failure, returns default - if face_output_format is not None: - if face_output_format not in _VALID_FACE_OUTPUT_FORMATS: - logger.warning( - "FaceDetectionNode: invalid face_output_format='%s', " - "using '%s' instead. Valid: %s", - face_output_format, _DEFAULT_FACE_OUTPUT_FORMAT, - list(_VALID_FACE_OUTPUT_FORMATS), - ) return True @classmethod @@ -784,6 +834,26 @@ class FaceDetectionNodeV1: output_height = _coerce_int(output_height, "output_height", 512) instance_id = str(instance_id) if instance_id is not None else "0" padding = _coerce_int(padding, "padding", 0) + + # Defensive: sanitize combo fields that may have wrong values from + # legacy workflows with misaligned positional widgets_values + if aspect_ratio not in _VALID_ASPECT_RATIOS: + logger.warning("FaceDetectionNode: aspect_ratio='%s' invalid, using '%s'", + aspect_ratio, _DEFAULT_ASPECT_RATIO) + aspect_ratio = _DEFAULT_ASPECT_RATIO + if output_mode not in _VALID_OUTPUT_MODES: + logger.warning("FaceDetectionNode: output_mode='%s' invalid, using '%s'", + output_mode, _DEFAULT_OUTPUT_MODE) + output_mode = _DEFAULT_OUTPUT_MODE + if classifier_type not in _VALID_CLASSIFIER_TYPES: + logger.warning("FaceDetectionNode: classifier_type='%s' invalid, using '%s'", + classifier_type, _DEFAULT_CLASSIFIER_TYPE) + classifier_type = _DEFAULT_CLASSIFIER_TYPE + if face_output_format not in _VALID_FACE_OUTPUT_FORMATS: + logger.warning("FaceDetectionNode: face_output_format='%s' invalid, using '%s'", + face_output_format, _DEFAULT_FACE_OUTPUT_FORMAT) + face_output_format = _DEFAULT_FACE_OUTPUT_FORMAT + cascade = CascadeCache.get(classifier_type) if cascade is None: logger.error("No cascade available") @@ -800,14 +870,6 @@ class FaceDetectionNodeV1: else: pad_ratio = auto_padding_ratio / 100.0 - # Resolve face_output_format with fallback - if face_output_format not in _VALID_FACE_OUTPUT_FORMATS: - logger.warning( - "FaceDetectionNode: invalid face_output_format='%s', using '%s'", - face_output_format, _DEFAULT_FACE_OUTPUT_FORMAT, - ) - face_output_format = _DEFAULT_FACE_OUTPUT_FORMAT - detect_all = (output_mode == "all_faces") temporal = self._get_temporal(instance_id, temporal_smoothing) diff --git a/pyproject.toml b/pyproject.toml index 83e7f3d..95399d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_FaceDetectionNode" description = "H100-optimized face detection with Auto-Padding, Temporal Smoothing, Aspect Ratio Presets, and full batch processing for LTX-Video avatar pipelines. Backward compatible with v1.x workflows." -version = "2.1.2" +version = "2.1.3" license = {text = "Apache-2.0"} dependencies = ["opencv-python>=4.5.0", "torch>=2.0.0", "numpy>=1.21.0"]