Files
ryanontheinside-ComfyUI_Rea…/node_wrappers/face/blendshape.py

179 lines
9.4 KiB
Python

# node_wrappers/face/blendshape.py
import logging
from typing import Optional, List, Any, Dict, Tuple
from ...src.types import BLENDSHAPES_LIST, Blendshape # Updated type alias
from ...src.utils.misc_utils import get_blendshape_categories # Utility to get all category names
# Import base trigger/control logic if we adapt base classes later, or implement directly for now
from ..common.base_delta_nodes import FLOAT_EQUALITY_TOLERANCE # For trigger node comparisons
from ...src.utils.delta_utils import scale_value
logger = logging.getLogger(__name__)
# --- Constants ---
# Get the list of available blendshape categories from MediaPipe
ALL_BLENDSHAPE_CATEGORIES = get_blendshape_categories()
if not ALL_BLENDSHAPE_CATEGORIES:
logger.warning("Could not retrieve blendshape categories. Blendshape nodes might not function correctly.")
ALL_BLENDSHAPE_CATEGORIES = ["_neutral"] # Provide a fallback
_category = "MediaPipeVision/Face/Blendshape"
# --- Helper Functions ---
def get_blendshape_score(blendshapes_result: BLENDSHAPES_LIST, category_name: str) -> Optional[float]:
"""Extracts the score for a specific blendshape category from the result."""
if not blendshapes_result or not blendshapes_result[0] or not blendshapes_result[0][0]:
#logger.debug(f"No blendshapes found in result.")
return None # No blendshape data
# BLENDSHAPES structure: List[List[Dict[str, float]]]
# We typically care about the first face in the first batch item
first_face_blendshapes = blendshapes_result[0][0]
# In the MediaPipe output, blendshapes are represented as a dictionary
# where keys are category names and values are scores
if isinstance(first_face_blendshapes, dict):
# Dictionary format: {category_name: score, ...}
if category_name in first_face_blendshapes:
return first_face_blendshapes[category_name]
elif isinstance(first_face_blendshapes, list):
# Try to handle the list format with objects that have category_name attribute
try:
target_blendshape = next((bs for bs in first_face_blendshapes if hasattr(bs, 'category_name') and bs.category_name == category_name), None)
if target_blendshape:
return target_blendshape.score
except Exception as e:
logger.error(f"Error processing blendshape list: {e}")
logger.warning(f"Blendshape category '{category_name}' not found in results. Type: {type(first_face_blendshapes)}")
return None
# --- Control Nodes ---
class BlendshapeControlFloatNode:
"""Outputs a scaled FLOAT based on the score of a selected blendshape."""
CATEGORY = _category
RETURN_TYPES = ("FLOAT",)
FUNCTION = "execute"
DESCRIPTION = "Converts facial expressions into floating point values that can control other nodes. For example, use a smile to control image parameters or animation intensity."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"blendshapes": ("BLENDSHAPES_LIST", {"tooltip": "Facial expression data from the Face Landmarker node"}),
"blendshape_name": (ALL_BLENDSHAPE_CATEGORIES, {"default": "mouthSmileLeft",
"tooltip": "The specific facial expression to monitor (e.g., smile, frown, raised eyebrows)"}),
# Blendshape scores are typically 0.0 to 1.0, but allow flexibility
"score_min": ("FLOAT", {"default": 0.0, "step": 0.01,
"tooltip": "The minimum facial expression intensity to consider (usually 0.0)"}),
"score_max": ("FLOAT", {"default": 1.0, "step": 0.01,
"tooltip": "The maximum facial expression intensity to consider (usually 1.0)"}),
"output_min_float": ("FLOAT", {"default": 0.0, "step": 0.01,
"tooltip": "The minimum output value when expression is at or below score_min"}),
"output_max_float": ("FLOAT", {"default": 1.0, "step": 0.01,
"tooltip": "The maximum output value when expression is at or above score_max"}),
"clamp": ("BOOLEAN", {"default": True,
"tooltip": "When enabled, restricts output values to stay within min and max range"}),
}
}
def execute(self, blendshapes, blendshape_name, score_min, score_max, output_min_float, output_max_float, clamp):
raw_score = get_blendshape_score(blendshapes, blendshape_name)
scaled_value = scale_value(raw_score, score_min, score_max, output_min_float, output_max_float, clamp)
# Default to min value on error or if blendshape not found
output_val = scaled_value if scaled_value is not None else float(output_min_float)
return (output_val,)
class BlendshapeControlIntNode:
"""Outputs a scaled INT based on the score of a selected blendshape."""
CATEGORY = _category
RETURN_TYPES = ("INT",)
FUNCTION = "execute"
DESCRIPTION = "Converts facial expressions into integer values that can control other nodes. Useful for selecting steps, seeds, or indexes based on facial expressions."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"blendshapes": ("BLENDSHAPES_LIST", {"tooltip": "Facial expression data from the Face Landmarker node"}),
"blendshape_name": (ALL_BLENDSHAPE_CATEGORIES, {"default": "mouthSmileLeft",
"tooltip": "The specific facial expression to monitor (e.g., smile, frown, raised eyebrows)"}),
"score_min": ("FLOAT", {"default": 0.0, "step": 0.01,
"tooltip": "The minimum facial expression intensity to consider (usually 0.0)"}),
"score_max": ("FLOAT", {"default": 1.0, "step": 0.01,
"tooltip": "The maximum facial expression intensity to consider (usually 1.0)"}),
"output_min_int": ("INT", {"default": 0,
"tooltip": "The minimum integer output value when expression is at or below score_min"}),
"output_max_int": ("INT", {"default": 100,
"tooltip": "The maximum integer output value when expression is at or above score_max"}),
"clamp": ("BOOLEAN", {"default": True,
"tooltip": "When enabled, restricts output values to stay within min and max range"}),
}
}
def execute(self, blendshapes, blendshape_name, score_min, score_max, output_min_int, output_max_int, clamp):
raw_score = get_blendshape_score(blendshapes, blendshape_name)
scaled_value = scale_value(raw_score, score_min, score_max, output_min_int, output_max_int, clamp)
# Default to min value on error or if blendshape not found
output_val = int(round(scaled_value)) if scaled_value is not None else int(output_min_int)
return (output_val,)
# --- Trigger Node ---
class BlendshapeTriggerNode:
"""Outputs a BOOLEAN trigger if a blendshape score crosses a threshold."""
CATEGORY = _category
RETURN_TYPES = ("BOOLEAN",)
FUNCTION = "execute"
DESCRIPTION = "Creates a true/false trigger based on facial expressions exceeding thresholds. Perfect for activating workflows when someone smiles, frowns, or makes specific expressions."
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"blendshapes": ("BLENDSHAPES_LIST", {"tooltip": "Facial expression data from the Face Landmarker node"}),
"blendshape_name": (ALL_BLENDSHAPE_CATEGORIES, {"default": "jawOpen",
"tooltip": "The specific facial expression to monitor (e.g., smile, frown, open mouth)"}),
"threshold": ("FLOAT", {"default": 0.5, "step": 0.01,
"tooltip": "The expression intensity threshold value that triggers the condition"}),
"condition": (["Above", "Below", "Equals", "Not Equals"],
{"tooltip": "The condition that determines when to trigger - Above (expression > threshold), Below (expression < threshold), etc."}),
}
}
def execute(self, blendshapes, blendshape_name, threshold, condition):
raw_score = get_blendshape_score(blendshapes, blendshape_name)
triggered = False
if raw_score is not None:
if condition == "Above" and raw_score > threshold:
triggered = True
elif condition == "Below" and raw_score < threshold:
triggered = True
elif condition == "Equals" and abs(raw_score - threshold) < FLOAT_EQUALITY_TOLERANCE:
triggered = True
elif condition == "Not Equals" and abs(raw_score - threshold) >= FLOAT_EQUALITY_TOLERANCE:
triggered = True
return (triggered,)
# --- Mappings ---
NODE_CLASS_MAPPINGS = {
"BlendshapeControlFloat": BlendshapeControlFloatNode,
"BlendshapeControlInt": BlendshapeControlIntNode,
"BlendshapeTrigger": BlendshapeTriggerNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BlendshapeControlFloat": "Blendshape Control (Float)",
"BlendshapeControlInt": "Blendshape Control (Int)",
"BlendshapeTrigger": "Blendshape Trigger",
}