Artfat Face Consistency (Sort)
The face-consistency gate that sorts your good frames from your bad ones
- image
- reference_image
- image
- similarity
- passed
- verdict
- annotated
You've been here. You run your persona LoRA, batch out a dozen frames, and three of them are almost your person - same outfit, same pose, but the model quietly swapped in a stranger's face. So you squint at each one and delete the misses by hand. Artfat Face Consistency (Sort) is that squinting, automated. Drop it after your VAE decode and it scores every frame against a reference face, stamps a verdict on it, and files it into a pass or fail folder while you do something else.
Be clear-eyed about what it is: this is a curation node, not a consistency fixer. It won't make faces more alike - it sorts what you already generated. That still matters, because "generate then manually reject the drift" is exactly how most character-consistency workflows actually end, whatever the marketing says about your adapter.
How it works
The scoring core is OpenCV's YuNet detector plus SFace recognizer, pure cv2 + numpy running on CPU. It never touches VRAM, so it doesn't crowd your sampler - you can gate every frame in a batch without stealing memory from generation. YuNet finds the biggest face in frame, SFace aligns and embeds it into a vector, and the node compares that vector to your reference using cosine similarity. Same person scores around 0.363 on SFace's own scale; the closer to 1.0, the stronger the match.
Feed reference_image as a batch of 1..N shots and they get averaged into a single centroid - a dataset centroid is a steadier anchor than any one frame. You need some reference: without one, the node raises an error rather than guessing.
The inputs that actually matter
image- the frame(s) to check, usually the VAE-decoded output.min_similarity- the flat pass bar (default0.5). Since same-id starts around0.363,0.45–0.6is a sensible gate for a strong persona; crank it if your LoRA lands hard.pass_dir/fail_dir- where frames get filed. Both are created if missing.reference_image(orreference_folder) - the identity anchor. Connect this or the node throws.
On the output side you get image (passthrough), similarity (FLOAT), passed (BOOLEAN), verdict (STRING) and annotated - an IMAGE with a score badge drawn in, handy for eyeballing what it flagged. The score is baked into the filename it writes, so your pass folder reads like blanca_00007_sim0.782_pass.png.
Adaptive thresholds and the borderline band
One flat threshold is unfair across shot sizes: a small face embeds from fewer pixels, so a full-body frame of the right person legitimately scores lower than a close-up of the wrong one. Turn on adaptive_threshold and the bar scales with how big the face is in frame (face_frac) - sim_large/sim_small at frac_large/frac_small, linear between. It's off by default, so existing graphs behave exactly as before. Set borderline_margin above 0 and frames that just miss get routed to borderline_dir for a human to review instead of being condemned.
Install
Grab it in ComfyUI Manager (search "Artfat Face Consistency"), or:
cd ComfyUI/custom_nodes
git clone https://github.com/artfat-creator/ComfyUI-Artfat-FaceConsistency
Restart ComfyUI and the nodes appear under the artfat category. Dependencies are opencv-python and numpy, both of which ship with ComfyUI - the yunet.onnx / sface.onnx models are bundled in the pack's models/, so there's nothing extra to download. The optional YOLO detector (more robust on small/angled faces in full-body shots) wants ultralytics plus a face .pt file in models/ultralytics/bbox/ - if you already run Impact-Pack or ADetailer, you very likely have one.
Troubleshooting
- Nodes don't show up, or load broken after an update - fully restart ComfyUI, and if that's not enough right-click → Fix node (recreate). The README calls this normal after inputs change.
- "Connect a reference_image or set reference_folder" - you fed it frames but no anchor. Wire one in.
- Score near zero (< 0.1) - that's usually SFace failing to embed the crop, a detector glitch, not an identity mismatch. Don't tune thresholds to it.
- Small faces score low - flip on
adaptive_threshold, or switchdetectorto YOLO so the face gets cropped and re-aligned before scoring.
One more thing worth knowing: this is one of the rare face-identity tools with a clean license. No InsightFace models, no non-commercial checkpoints - the whole pack is MIT. If you've ever hit the InsightFace licensing wall with InstantID or PuLID pipelines, this one's refreshingly uncomplicated. Set a csv_path on your first run and read where your persona's real frames land before you pick a gate.
Inputs (21)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | Frame(s) to check — usually the VAE-decoded output. | |
| min_similarity | FLOAT | 0.50-1–1 | Flat pass threshold (used when adaptive_threshold is OFF). Cosine >= this -> pass. SFace same-id starts ~0.363; for a strong persona 0.45-0.6 is a sensible gate. |
| pass_dir | STRING | Folder for images that PASS. Created if missing. | |
| fail_dir | STRING | Folder for images that FAIL (and no-face). Created if missing. | |
| reference_imageopt | IMAGE | Reference identity, 1..N images (batch). Averaged into a centroid. | |
| reference_folderopt | STRING | Optional folder of reference images, merged with reference_image. | |
| filename_prefixopt | STRING | blanca | — |
| csv_pathopt | STRING | Optional CSV log path (appended). Logs face_frac + threshold for calibration. Empty = no log. | |
| save_filesopt | BOOLEAN | true | Off = only score/pass outputs, nothing written to disk. |
| adaptive_thresholdopt | BOOLEAN | false | ON = pass threshold scales with face size (close-ups judged strictly, small/far faces leniently). OFF = flat min_similarity (old behaviour). |
| sim_largeopt | FLOAT | 0.60-1–1 | Adaptive: threshold for LARGE/close faces (face_frac >= frac_large). |
| sim_smallopt | FLOAT | 0.50-1–1 | Adaptive: threshold for SMALL/far faces (face_frac <= frac_small). |
| frac_largeopt | FLOAT | 0.150–1 | Adaptive: face-area fraction at/above which sim_large applies. |
| frac_smallopt | FLOAT | 0.030–1 | Adaptive: face-area fraction at/below which sim_small applies. |
| borderline_marginopt | FLOAT | 0.000–0.5 | 0 = off. Else frames JUST BELOW the threshold (within this margin) go to 'borderline' (borderline_dir) for review. At/above threshold = pass; the band is one-sided. |
| borderline_diropt | STRING | Folder for borderline frames. Empty -> they go to fail_dir. | |
| detectoropt | COMBO | YuNet (fast, CPU) | YuNet = fast cv2, great on portraits. YOLO = finds small faces in full-body/complex shots, then crops+re-aligns so the score is fair. |
| yolo_modelopt | COMBO | Face .pt model used when detector = YOLO. | |
| detect_confopt | FLOAT | 0.500.05–0.95 | YOLO confidence. LOWER = catches harder/smaller/angled faces. |
| crop_paddingopt | FLOAT | 0.300–1 | Expand the detected face box by this fraction before scoring (more context). |
| min_face_fracopt | FLOAT | 0.000–0.5 | Ignore faces smaller than this fraction of the frame (0 = off). Skips background faces. |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| similarity | FLOAT | — |
| passed | BOOLEAN | — |
| verdict | STRING | — |
| annotated | IMAGE | — |