Face QC
One comparison, two verdicts, zero AI involved
- passed
- verdict
Strip the name off this node and it's a threshold gate: is this number at least as big as that number? True or False, plus a line of text you wrote yourself.
It sits in the pack's Face category because that's what it was built for - taking the cosine score out of a face-similarity comparison and turning it into a pass/fail verdict. But nothing in the node knows about faces, and the author says so in the description. Feed it a CLIP similarity, an aesthetic score, a sharpness measurement or a file size and it works exactly the same.
That's a good design. The judgement call lives in one place, and the expensive part lives in the node that produces the number.
Inputs
score(FLOAT, default 0, range -1 to 2, step 0.001) - the measured value.threshold(FLOAT, default 0.6, range -1 to 2, step 0.01) - the line it has to clear.pass_text(STRING, optional, defaultSame person) - the verdict string when it passes.fail_text(STRING, optional, defaultIdentity drift detected) - the verdict string when it doesn't.
Outputs
- passed (BOOLEAN) - true when
score >= threshold. Note the inclusive comparison: a score landing exactly on 0.6 passes. - verdict (STRING) - one of your two strings.
Why ±2 on the range
The bounds are worth a second's thought, because they encode what the node expects to see. Cosine similarity from an ArcFace-style embedding lives in roughly -1 to 1, and a threshold of 0.6 is the usual starting point for "same person." But plenty of other metrics are bounded 0–1, and some blur scores drift above 1, so a range topping out at 2 keeps the widget from fighting you when you wire something unusual into it.
Both score and threshold accept wiring, which is what makes this composable - you can drive the threshold from another node, or from a primitive you change once, instead of editing the widget.
Setting the threshold honestly
This is where people go wrong, and it's not the node's fault. A similarity score from an embedding model is a measure of geometric closeness in a vector space, not a statement about identity. Two photos of the same person in very different lighting and expression can score below a pair of siblings photographed identically. The number is useful relative to other runs of your own pipeline, with your own references - which is exactly what Face QC is for, and why it's a workflow gate rather than a face-recognition system.
Practically: run ten or twenty generations of a character you know is correct, look at the spread of scores, and set the threshold where the good ones live. Don't import a number from a forum post - the surrounding identity-preservation stack (IP-Adapter FaceID, InstantID, PuLID) all sits on the same embedding foundation, and every one of those tools has its own score distribution.
Wiring it
Multi Reference Face Similarity → average_score → Face QC.score
Face QC.passed → the selector on a lazy switch (see Pass / Fail Route)
Face QC.verdict → Workflow Status.verdict, a filename, or a caption
Install
Manager → search Orel's Nodes → Install → restart → hard-refresh the browser.
cd ComfyUI/custom_nodes
git clone https://github.com/DarkOrel/Orels-Nodes.git orels-nodes
python -m pip install -r orels-nodes/requirements.txt
Restart and hard-refresh. Important: this node needs no optional dependencies. It doesn't touch InsightFace, OpenCV or ONNX - it's arithmetic and a string pick. That's why it keeps working when the pack's face-similarity node can't find its packages.
Gotchas
Empty strings silently revert to defaults. If you clear pass_text, you don't get a blank verdict - you get Same person back. Deliberate, but surprising the first time.
It does not look at the image. No face is detected here, nothing is verified. Wire a score in and you get a verdict; wire nothing in and you get a verdict about the default of 0.
No hysteresis. Another node could ping-pong around the threshold run to run and the gate will report flip-flopping pass/fail with equal confidence each time. If that's happening, your threshold is sitting in the middle of your normal variance - move it.
It won't stop downstream nodes. The boolean is just a boolean. To actually skip work, drive a lazy switch from passed; see Pass / Fail Route for why that distinction bites.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| score | FLOAT | 0.000-1–2 | — |
| threshold | FLOAT | 0.60-1–2 | — |
| pass_textopt | STRING | Same person | — |
| fail_textopt | STRING | Identity drift detected | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| passed | BOOLEAN | — |
| verdict | STRING | — |