ComfyUI Node

Face QC

One comparison, two verdicts, zero AI involved

By DarkOrel·Created 30 days ago·Updated 22 days ago· 1
Face QC
    • passed
    • verdict
    ◄score0.000►
    ◄threshold0.60►
    ◄pass_textSame person►
    ◄fail_textIdentity drift detected►

    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, default Same person) - the verdict string when it passes.
    • fail_text (STRING, optional, default Identity 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.

    CategoryOrel's Nodes/Face

    Inputs (4)

    NameTypeDefaultDescription
    scoreFLOAT0.000-1–2—
    thresholdFLOAT0.60-1–2—
    pass_textoptSTRINGSame person—
    fail_textoptSTRINGIdentity drift detected—

    Outputs (2)

    NameTypeDescription
    passedBOOLEAN—
    verdictSTRING—