Face Performance Evaluate
Score your generated facial performance so you stop eyeballing lip sync
- metrics_json
- failures_json
- lag_frames
- blink_rate_per_minute
- pose_jitter_score
- summary
Every generated facial performance eventually needs a QC pass, and doing that by eye across hundreds of frames is a slow way to go blind. MKRFacePerformanceEvaluate is the MKRShift node that scores a clip numerically instead: lip/audio sync lag, blink rate, landmark smoothness, and pose jitter - then flags anything that breaks a threshold. It's the regression-test and QA node at the end of the face-performance pipeline.
If you've ever changed one retarget setting, re-rendered, and squinted at the mouth to decide if it's "better," this node exists to replace that guess with a number. Same idea as the pack's other analysis nodes (the freeze-frame detector, the scene-cut detector): measure it, don't feel it.
How it works
You feed it the same JSON frame arrays the other performance nodes produce, and it computes metrics:
audio_frames_json- source audio features.refined_frames_json- fromMKRFacePerformanceLipRefine(or compatible data).eye_frames_json- fromMKRFacePerformanceEyeMotion.pose_frames_json- fromMKRFacePerformancePoseMerge.
clip_id is just a label stored in the metrics so you can tell runs apart (default clip_alpha - rename it per shot). fps (default 60) drives all the timing math, so it must match the pipeline you're evaluating.
The evaluation utility (lib/face_performance/evaluation_utility.py) computes lip/audio sync lag in frames, blink behavior, motion outliers, and pose jitter. The outputs that actually matter:
lag_frames(INT) - best-estimate lip-to-audio lag in frames. The headline number.blink_rate_per_minute(FLOAT) - estimated blink rate; humans land roughly 10–20/min, so wildly different numbers suggest the eye synthesis is doing something odd.pose_jitter_score(FLOAT) - mean per-frame pose delta across head and neck channels. High = wobbly.metrics_json- the full metric object (also whereclip_idshows up).failures_json- array of threshold violations, fromthresholds_json. Leavethresholds_jsonas{}to use the built-in defaults; pass overrides for anyRegressionThresholdsfield when you have project-specific tolerances.
What it's for
The author's own framing says it best: it's for regression testing or comparing retarget settings inside a workflow. That's the killer use case - set up a baseline render, tweak a knob, re-evaluate, and read the numbers instead of re-watching both renders frame by frame. If you're iterating on max_delta_per_frame or blink intervals and want to know if you made things better or just different, this is the honest arbiter.
Installing and caveats
Standard pack install:
cd ComfyUI/custom_nodes
git clone https://github.com/criskb/MKRShift_Nodes
Restart ComfyUI, or ComfyUI Manager search "MKRShift_Nodes". Pure Python, no new deps.
Caveats are the lane's usual ones: it's for structured JSON performance data, not raw video (all inputs must be JSON arrays, thresholds_json a JSON object - the validator checks). And there's zero community chatter about it yet, so treat the built-in thresholds as starting points, not gospel. But for its stated job - "did this change make the performance better or worse" - a numeric score beats vibes, and this node gives you one.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| clip_id | STRING | clip_alpha | — |
| audio_frames_json | STRING | [] | — |
| refined_frames_json | STRING | [] | — |
| eye_frames_json | STRING | [] | — |
| pose_frames_json | STRING | [] | — |
| fps | INT | 601–240 | — |
| thresholds_json | STRING | {} | — |
Outputs (6)
| Name | Type | Description |
|---|---|---|
| metrics_json | STRING | — |
| failures_json | STRING | — |
| lag_frames | INT | — |
| blink_rate_per_minute | FLOAT | — |
| pose_jitter_score | FLOAT | — |
| summary | STRING | — |