Co-authored-by: EricRorich <86147326+EricRorich@users.noreply.github.com>
344 lines
12 KiB
Python
344 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Validation script for the ComfyUI Face Shaper changes.
|
|
|
|
This script validates:
|
|
1. Updated ear geometry from new SVG
|
|
2. Removal of obsolete nose_tip geometry
|
|
3. Integration of nose tip control within nose polyline
|
|
4. Correct settings list handling
|
|
|
|
Run this without dependencies installed to validate the code structure.
|
|
"""
|
|
|
|
import sys
|
|
import ast
|
|
import re
|
|
|
|
def check_female_face_dict():
|
|
"""Validate FEMALE_FACE dictionary."""
|
|
print("=" * 60)
|
|
print("1. Validating FEMALE_FACE dictionary")
|
|
print("=" * 60)
|
|
|
|
with open('face_shaper.py', 'r') as f:
|
|
content = f.read()
|
|
|
|
# Extract FEMALE_FACE dictionary
|
|
start = content.find('FEMALE_FACE: Dict[str, List[Tuple[float, float]]] = {')
|
|
end = content.find('\n}', start) + 2
|
|
female_face_code = content[start:end]
|
|
|
|
# Execute it in a local namespace
|
|
namespace = {'Dict': dict, 'List': list, 'Tuple': tuple}
|
|
exec(female_face_code, namespace)
|
|
FEMALE_FACE = namespace['FEMALE_FACE']
|
|
|
|
# Check ear coordinates are updated
|
|
ear_left = FEMALE_FACE.get('ear_left', [])
|
|
ear_right = FEMALE_FACE.get('ear_right', [])
|
|
|
|
if not ear_left or not ear_right:
|
|
print("✗ FAILED: ear_left or ear_right missing")
|
|
return False
|
|
|
|
# Verify updated coordinates from new SVG (path184, subpath 2 for ear_right)
|
|
# SVG-derived ear_right starts at (0.796875, 0.609375)
|
|
if abs(ear_right[0][0] - 0.796875) < 0.001:
|
|
print("✓ ear_right has updated SVG coordinates")
|
|
else:
|
|
print(f"✗ FAILED: ear_right[0][0] = {ear_right[0][0]}, expected ~0.796875")
|
|
return False
|
|
|
|
# SVG-derived ear_left starts at (0.187500, 0.484375)
|
|
if abs(ear_left[0][0] - 0.187500) < 0.001:
|
|
print("✓ ear_left has updated SVG coordinates")
|
|
else:
|
|
print(f"✗ FAILED: ear_left[0][0] = {ear_left[0][0]}, expected ~0.187500")
|
|
return False
|
|
|
|
# Verify ear point counts (5 points each from SVG path184)
|
|
if len(ear_right) != 5:
|
|
print(f"✗ FAILED: ear_right should have 5 points, got {len(ear_right)}")
|
|
return False
|
|
else:
|
|
print(f"✓ ear_right has 5 points (correct)")
|
|
|
|
if len(ear_left) != 5:
|
|
print(f"✗ FAILED: ear_left should have 5 points, got {len(ear_left)}")
|
|
return False
|
|
else:
|
|
print(f"✓ ear_left has 5 points (correct)")
|
|
|
|
# Check nose_tip is removed
|
|
if 'nose_tip' in FEMALE_FACE:
|
|
print("✗ FAILED: nose_tip geometry should be removed")
|
|
return False
|
|
else:
|
|
print("✓ nose_tip geometry successfully removed")
|
|
|
|
# Check nose still exists with 13 points (updated from SVG path46)
|
|
if 'nose' not in FEMALE_FACE or len(FEMALE_FACE['nose']) != 13:
|
|
print(f"✗ FAILED: nose should have 13 points, got {len(FEMALE_FACE.get('nose', []))}")
|
|
return False
|
|
else:
|
|
print(f"✓ nose has 13 points (correct, from SVG path46)")
|
|
|
|
print()
|
|
return True
|
|
|
|
def check_input_types():
|
|
"""Validate INPUT_TYPES parameters."""
|
|
print("=" * 60)
|
|
print("2. Validating INPUT_TYPES parameters")
|
|
print("=" * 60)
|
|
|
|
with open('face_shaper.py', 'r') as f:
|
|
content = f.read()
|
|
|
|
# Find INPUT_TYPES method
|
|
pattern = r'def INPUT_TYPES\(cls\):.*?return\s+\{.*?\n\s+\}'
|
|
match = re.search(pattern, content, re.DOTALL)
|
|
if not match:
|
|
print("✗ FAILED: Could not find INPUT_TYPES method")
|
|
return False
|
|
|
|
input_types_code = match.group(0)
|
|
|
|
# Check nose_tip_pos_y is present with correct range
|
|
if 'nose_tip_pos_y' not in input_types_code:
|
|
print("✗ FAILED: nose_tip_pos_y missing")
|
|
return False
|
|
|
|
if '"min": -0.2' in input_types_code and '"max": 0.2' in input_types_code:
|
|
print("✓ nose_tip_pos_y present with correct range (-0.2 to 0.2)")
|
|
else:
|
|
print("✗ FAILED: nose_tip_pos_y has incorrect range")
|
|
return False
|
|
|
|
# Check nose_tip_size_x and nose_tip_size_y are removed
|
|
if 'nose_tip_size_x' in input_types_code or 'nose_tip_size_y' in input_types_code:
|
|
print("✗ FAILED: nose_tip_size_x or nose_tip_size_y should be removed")
|
|
return False
|
|
else:
|
|
print("✓ nose_tip_size_x and nose_tip_size_y successfully removed")
|
|
|
|
# Check ear parameters are present
|
|
ear_params = ['ear_left_pos_x', 'ear_left_pos_y', 'ear_left_size_x', 'ear_left_size_y',
|
|
'ear_right_pos_x', 'ear_right_pos_y', 'ear_right_size_x', 'ear_right_size_y']
|
|
missing = [p for p in ear_params if p not in input_types_code]
|
|
if missing:
|
|
print(f"✗ FAILED: Missing ear parameters: {missing}")
|
|
return False
|
|
else:
|
|
print("✓ All ear parameters present")
|
|
|
|
print()
|
|
return True
|
|
|
|
def check_draw_face_signature():
|
|
"""Validate draw_face method signature."""
|
|
print("=" * 60)
|
|
print("3. Validating draw_face signature")
|
|
print("=" * 60)
|
|
|
|
with open('face_shaper.py', 'r') as f:
|
|
source = f.read()
|
|
|
|
tree = ast.parse(source)
|
|
|
|
# Find draw_face method
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef) and node.name == 'draw_face':
|
|
params = [arg.arg for arg in node.args.args]
|
|
|
|
# Check nose_tip_pos_y is present
|
|
if 'nose_tip_pos_y' not in params:
|
|
print("✗ FAILED: nose_tip_pos_y missing from signature")
|
|
return False
|
|
else:
|
|
print("✓ nose_tip_pos_y present in signature")
|
|
|
|
# Check nose_tip_size_x and nose_tip_size_y are removed
|
|
if 'nose_tip_size_x' in params or 'nose_tip_size_y' in params:
|
|
print("✗ FAILED: nose_tip_size parameters should be removed")
|
|
return False
|
|
else:
|
|
print("✓ nose_tip_size_x and nose_tip_size_y removed from signature")
|
|
|
|
# Check ear parameters
|
|
ear_params = ['ear_left_pos_x', 'ear_left_pos_y', 'ear_left_size_x', 'ear_left_size_y',
|
|
'ear_right_pos_x', 'ear_right_pos_y', 'ear_right_size_x', 'ear_right_size_y']
|
|
missing = [p for p in ear_params if p not in params]
|
|
if missing:
|
|
print(f"✗ FAILED: Missing ear parameters: {missing}")
|
|
return False
|
|
else:
|
|
print("✓ All ear parameters present in signature")
|
|
|
|
print()
|
|
return True
|
|
|
|
print("✗ FAILED: Could not find draw_face method")
|
|
return False
|
|
|
|
def check_nose_rendering():
|
|
"""Validate nose rendering logic."""
|
|
print("=" * 60)
|
|
print("4. Validating nose rendering logic")
|
|
print("=" * 60)
|
|
|
|
with open('face_shaper.py', 'r') as f:
|
|
content = f.read()
|
|
|
|
# Find the nose rendering section
|
|
nose_section_pattern = r'# Draw nose.*?draw\.line\(pixel_points.*?\)'
|
|
matches = list(re.finditer(nose_section_pattern, content, re.DOTALL))
|
|
|
|
if len(matches) != 1:
|
|
print(f"✗ FAILED: Should have exactly 1 nose drawing section, found {len(matches)}")
|
|
return False
|
|
else:
|
|
print("✓ Nose is drawn exactly once")
|
|
|
|
nose_code = matches[0].group(0)
|
|
|
|
# Check that the code algorithmically identifies tip points
|
|
# (sorting by Y descending, then by distance from center)
|
|
if 'sorted_points = sorted(indexed_points, key=lambda p: (-p[2], abs(p[1] - face_center_x)))' in nose_code:
|
|
print("✓ nose_tip_indices algorithmically identified (sorts by Y descending, then centrality)")
|
|
elif 'nose_tip_indices = {5, 6, 7}' in nose_code:
|
|
print("✓ nose_tip_indices correctly set to {5, 6, 7}")
|
|
elif 'nose_tip_indices = {3, 5, 7}' in nose_code:
|
|
print("⚠ WARNING: nose_tip_indices set to old values {3, 5, 7}, should be {5, 6, 7}")
|
|
print(" (This may still work with the algorithm)")
|
|
else:
|
|
print("✗ FAILED: nose_tip_indices not correctly identified")
|
|
return False
|
|
|
|
# Check that it applies the offset
|
|
if 'y + nose_tip_pos_y' in nose_code:
|
|
print("✓ nose_tip_pos_y offset applied to tip points")
|
|
else:
|
|
print("✗ FAILED: nose_tip_pos_y offset not applied")
|
|
return False
|
|
|
|
# Check that there's no separate nose_tip rendering
|
|
# (excluding comments and docstrings)
|
|
remaining = content[matches[0].end():]
|
|
remaining_no_comments = re.sub(r'#.*', '', remaining)
|
|
remaining_no_comments = re.sub(r'""".*?"""', '', remaining_no_comments, flags=re.DOTALL)
|
|
|
|
if 'face_points["nose_tip"]' in remaining_no_comments:
|
|
print("✗ FAILED: Separate nose_tip rendering still exists")
|
|
return False
|
|
else:
|
|
print("✓ No separate nose_tip rendering")
|
|
|
|
print()
|
|
return True
|
|
|
|
def check_settings_list():
|
|
"""Validate settings list handling."""
|
|
print("=" * 60)
|
|
print("5. Validating settings list handling")
|
|
print("=" * 60)
|
|
|
|
with open('face_shaper.py', 'r') as f:
|
|
content = f.read()
|
|
|
|
# Check SETTINGS_LIST_LENGTH
|
|
match = re.search(r'SETTINGS_LIST_LENGTH = (\d+)', content)
|
|
if match:
|
|
length = int(match.group(1))
|
|
if length == 60:
|
|
print(f"✓ SETTINGS_LIST_LENGTH = 60 (allows for 57 used + 3 reserved)")
|
|
else:
|
|
print(f"✗ FAILED: SETTINGS_LIST_LENGTH = {length}, expected 60")
|
|
return False
|
|
else:
|
|
print("✗ FAILED: Could not find SETTINGS_LIST_LENGTH")
|
|
return False
|
|
|
|
# Count parameters in settings_export
|
|
export_match = re.search(r'settings_export = \[(.*?)\]', content, re.DOTALL)
|
|
if export_match:
|
|
export_content = export_match.group(1)
|
|
params = [line.strip().rstrip(',') for line in export_content.split('\n')
|
|
if line.strip() and not line.strip().startswith('#')]
|
|
param_count = len(params)
|
|
|
|
if param_count == 57:
|
|
print(f"✓ settings_export has 57 parameters (correct)")
|
|
else:
|
|
print(f"✗ FAILED: settings_export has {param_count} parameters (expected 57)")
|
|
return False
|
|
|
|
# Check nose_tip_pos_y is present but not size parameters
|
|
if 'nose_tip_pos_y' in export_content:
|
|
print("✓ nose_tip_pos_y in settings_export")
|
|
else:
|
|
print("✗ FAILED: nose_tip_pos_y missing from settings_export")
|
|
return False
|
|
|
|
if 'nose_tip_size_x' in export_content or 'nose_tip_size_y' in export_content:
|
|
print("✗ FAILED: nose_tip size parameters should be removed from settings_export")
|
|
return False
|
|
else:
|
|
print("✓ nose_tip size parameters removed from settings_export")
|
|
else:
|
|
print("✗ FAILED: Could not find settings_export")
|
|
return False
|
|
|
|
# Check settings import indices
|
|
if 'nose_tip_pos_y = settings_list[49]' in content:
|
|
print("✓ nose_tip_pos_y at index 49 in import (correct)")
|
|
else:
|
|
print("✗ FAILED: nose_tip_pos_y should be at index 49 in import")
|
|
return False
|
|
|
|
if 'camera_distance = settings_list[50]' in content:
|
|
print("✓ camera_distance at index 50 in import (correct)")
|
|
else:
|
|
print("✗ FAILED: camera_distance should be at index 50 in import")
|
|
return False
|
|
|
|
print()
|
|
return True
|
|
|
|
def main():
|
|
"""Run all validation checks."""
|
|
print("\nComfyUI Face Shaper - Change Validation")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
results = []
|
|
|
|
results.append(("FEMALE_FACE dictionary", check_female_face_dict()))
|
|
results.append(("INPUT_TYPES parameters", check_input_types()))
|
|
results.append(("draw_face signature", check_draw_face_signature()))
|
|
results.append(("Nose rendering logic", check_nose_rendering()))
|
|
results.append(("Settings list handling", check_settings_list()))
|
|
|
|
print("=" * 60)
|
|
print("SUMMARY")
|
|
print("=" * 60)
|
|
|
|
all_passed = True
|
|
for name, passed in results:
|
|
status = "✓ PASS" if passed else "✗ FAIL"
|
|
print(f"{status}: {name}")
|
|
if not passed:
|
|
all_passed = False
|
|
|
|
print()
|
|
if all_passed:
|
|
print("✓ All validation checks passed!")
|
|
return 0
|
|
else:
|
|
print("✗ Some validation checks failed")
|
|
return 1
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|