Nodes/ComfyUI-DonutNodes/DMC Face Detailer · Ordered Characters
ComfyUI Node

DMC Face Detailer · Ordered Characters

DMC Face Detailer — Give Every Face in the Frame Its Own Prompt

By DonutsDelivery·Created 2 years ago·Updated 2 days ago· 27
DMC Face Detailer · Ordered Characters
  • image
  • model
  • clip
  • vae
  • positive
  • negative
  • bbox_detector
  • sam_model_opt
  • segm_detector_opt
  • detailer_hook
  • scheduler_func_opt
  • edit_model
  • face_reference
  • face_reference_b
  • nag_negative
  • nag_ref_boost_mask
  • image
  • cropped_refined
  • cropped_enhanced_alpha
  • mask
  • detailer_pipe
  • cnet_images
◄resolution1024►
◄max_resolution0►
◄guide_size_fortrue►
◄seed0►
◄steps20►
◄cfg8.00►
◄sampler_name▾►
◄scheduler▾►
◄denoise0.50►
◄feather5►
◄noise_masktrue►
◄force_inpainttrue►
◄bbox_threshold0.50►
◄bbox_dilation10►
◄bbox_crop_factor3.0►
◄sam_detection_hint▾►
◄sam_dilation0►
◄sam_threshold0.93►
◄sam_bbox_expansion0►
◄sam_mask_hint_threshold0.70►
◄sam_mask_hint_use_negative▾►
◄drop_size10►
◄wildcard—►
◄cycle1►
◄max_faces2►
◄inpaint_modelfalse►
◄noise_mask_feather20►
◄edit_modefalse►
◄edit_prompt—►
◄edit_negative_prompt—►
◄grounding_px768►
◄vary_seed_per_facefalse►
◄turbo_modefalse►
◄nag_enabledfalse►
◄nag_phi4.0►
◄nag_tau2.50►
◄nag_alpha0.25►
◄nag_sigma_start1000.0►
◄nag_sigma_end0.0►
◄nag_ref_boost1.00►
◄nag_ref_boost_a1.00►
◄nag_fit_modefit►
◄nag_auto_phifalse►
◄nag_phi_scale1.00►
◄vae_damage_correctionfalse►
◄vae_damage_strength1.00►
◄nag_alpha_scheduleconstant►
◄nag_alpha_start0.25►
◄nag_alpha_end0.25►
◄face_prompts_json[]►
◄require_single_facefalse►

A face detailer fixes smeared faces by cropping each one, re-rendering it bigger, and pasting it back. The catch nobody warns you about: every crop gets re-rendered with the same prompt, so a group shot comes back as a row of siblings. Hence the perennial "room full of clones" complaint.

DMCFaceDetailer is a specific fix for that. Give it a JSON list of characters - name, position, face-only prompt - and each detected face gets re-rendered with its own prompt. Same seed, same checkpoint, two genuinely different people.

How the routing actually works

It wraps the pack's own DonutFaceDetailer, so it inherits the whole Impact-style loop: detect boxes, optionally tighten them with SAM, crop, scale toward a target pixel budget, run a fresh masked sampling pass, paste back.

The wrapper adds two things. It detects faces once per image, then has to prove a matching between those faces and the characters you described - it doesn't guess. Then it runs the refinement pass one face at a time, fresh-encoding that character's face_prompt and swapping it in for the frame-level positive on that pass only. Your negative stays shared.

Ordering is the clever bit. Give positions as left/center/right or ordinals 1st–16th and it sorts by horizontal center - but only after checking each gap is wider than a quarter of the smaller face's width. Faces at nearly the same x get a refusal, not a coin flip. For awkward layouts, position can be an object with left_of and above arrays naming other characters, solved as a constraint problem.

So it won't silently process the wrong face: get the count wrong and you get Face routing mismatch: expected 2 characters but detected 2 faces, with no character prompts applied.

Note that the per-character prompt is the positive for that pass - not merged with your frame prompt, so keep that one about the scene.

The inputs worth touching

face_prompts_json is the point of the node, and it's a bare multiline text box - no rows, no picker. It takes 2 to 16 objects, each with exactly character, position and face_prompt:

[
  {"character": "Ada", "position": "left", "face_prompt": "Ada, green eyes, freckles, faint smile"},
  {"character": "Bo", "position": "right", "face_prompt": "Bo, thick beard, weathered skin, scowling"}
]

Duplicate names, a missing key, an empty prompt, or quotes pasted from a formatted doc are all rejected before anything samples. A single entry is an error too - use the plain DonutFaceDetailer for one face.

max_faces (default 2) caps how long the list may be; describe three characters with it at 2 and the node refuses rather than truncating, because dropping a face would shift everybody's identity. resolution (default 1024) is the pixel budget per crop's sampling canvas - the real cost dial - with max_resolution clamping the long edge. bbox_threshold (0.5) is what you lower when a face in a crowd isn't caught, and bbox_crop_factor (3) is how much context rides along. Flick on vary_seed_per_face for crowds.

Outputs: image is the frame with refined faces pasted in - wire that onward. mask is the combined mask of the matched faces. cropped_refined and cropped_enhanced_alpha are lists of per-face crops: fine for a preview grid, awkward anywhere expecting a batch. detailer_pipe feeds Impact's pipe nodes; cnet_images only fills if a hook emits control images.

edit_mode, face_reference and grounding_px do Krea 2 identity-edit passes, but only in the single-face path - with multi-character routing you get Multi-face identity editing and identity references are not supported.

Installing it

Easiest route is ComfyUI Manager: search DonutNodes, install or update, restart. The README is blunt that your installed package has to actually contain the node you're looking at - the "Donut Workflow V5" release name and the package version are different things. Manually:

cd ComfyUI/custom_nodes
git clone https://github.com/DonutsDelivery/ComfyUI-DonutNodes.git donutnodes
cd donutnodes
python -m pip install -r requirements.txt

Use the interpreter that launches ComfyUI. Requirements are light (opencv-python-headless, scipy, matplotlib, psutil, tqdm, requests), and the file warns against pinning NumPy globally. Only one OpenCV variant should be present.

What it genuinely needs on top is the Impact Pack - resolved at execution time, and it throws DonutFaceDetailer requires a working ComfyUI Impact Pack if missing. You also need a detector in bbox_detector: Ultralytics plus a face_yolov8-family model, typically. SAM is optional; sam_model_opt just tightens the masks.

Where people get burned

The count has to match. A profile shot or a hand across a face gives you a routing mismatch instead of an image. Drop bbox_threshold, nudge bbox_dilation up, and check your detector before blaming the JSON.

It's slower than it looks. Each character is its own sampling pass per image at resolution squared - four people across four images is sixteen passes. Drop resolution to 768 for group shots; the crops are already upscaled.

Don't pair it with wildcard or detailer_hook in multi-character mode. Both can replace the conditioning the routing just built, so the node rejects them rather than let a wildcard override your per-character prompts.

Mind your CFG. On a distilled Krea 2 Turbo at CFG 1 the negative is inert - true of every guidance-distilled model, not a bug here. Write constraints positively instead, or enable nag_enabled (needs the separate krea2-nag pack).

It routes, it doesn't recognize. Nothing checks that the face it refined belongs to the person you described - staggered faces can flip a left/right ordering and put correct prompts on the wrong people. Use the relative-position form there, and eyeball one render before queueing fifty.

Categorydonut/create

Inputs (67)

NameTypeDefaultDescription
imageIMAGE—
modelMODEL—
clipCLIP—
vaeVAE—
resolutionINT1024256–4096Equivalent square target size. Sampling canvases are snapped to 64-pixel multiples.
max_resolutionINT00–4096Maximum edge length (0 = no limit).
guide_size_forBOOLEANtrue—
seedINT00–18446744073709550000—
stepsINT201–10000—
cfgFLOAT8.000–100—
sampler_nameCOMBO44 options: euler, euler_cfg_pp, euler_ancestral, euler_ancestral_cfg_pp, heun, heunpp2, +38
schedulerCOMBO9 options: simple, sgm_uniform, karras, exponential, ddim_uniform, beta, +3
positiveCONDITIONING—
negativeCONDITIONING—
denoiseFLOAT0.500.0001–1—
featherINT50–100—
noise_maskBOOLEANtrue—
force_inpaintBOOLEANtrue—
bbox_thresholdFLOAT0.500–1—
bbox_dilationINT10-512–512—
bbox_crop_factorFLOAT3.01–10—
sam_detection_hintCOMBO9 options: center-1, horizontal-2, vertical-2, rect-4, diamond-4, mask-area, +3
sam_dilationINT0-512–512—
sam_thresholdFLOAT0.930–1—
sam_bbox_expansionINT00–1000—
sam_mask_hint_thresholdFLOAT0.700–1—
sam_mask_hint_use_negativeCOMBO3 options: False, Small, Outter
drop_sizeINT101–16384—
bbox_detectorBBOX_DETECTOR—
wildcardSTRING—
cycleINT11–10—
max_facesINT21–20Maximum number of faces to process (largest by area)
sam_model_optoptSAM_MODEL—
segm_detector_optoptSEGM_DETECTOR—
detailer_hookoptDETAILER_HOOK—
inpaint_modeloptBOOLEANfalse—
noise_mask_featheroptINT200–100—
scheduler_func_optoptSCHEDULER_FUNC—
edit_modeoptBOOLEANfalseUse Krea2 identity edit conditioning for each detected face.
edit_promptoptSTRING—
edit_modeloptMODELOptional Krea2 model with the Identity Edit LoRA already applied. Falls back to model.
face_referenceoptIMAGERequired in edit mode. The bbox detector extracts the identity face from this image.
edit_negative_promptoptSTRING—
grounding_pxoptINT7680–4096—
vary_seed_per_faceoptBOOLEANfalseUse a unique seed offset for each detected face.
turbo_modeoptBOOLEANfalseSnap denoise to a valid Turbo scheduler point.
face_reference_boptIMAGEOptional subject/identity image for two-reference edits. Faces are extracted from this image when possible; if it has no detectable face, the detailer falls back to face_reference.
nag_enabledoptBOOLEANfalseApply Krea2 NAG inside sampling (requires krea2-nag). Uses CFG 1; Turbo negative conditioning stays zeroed.
nag_negativeoptCONDITIONINGUnzeroed negative prompt for NAG. Fusion Rebalance/taps are applied to match the positive stream. Defaults to edit_negative_prompt in edit mode, otherwise negative.
nag_phioptFLOAT4.00–20—
nag_tauoptFLOAT2.500.01–20—
nag_alphaoptFLOAT0.250–1—
nag_sigma_startoptFLOAT1000.00–1000—
nag_sigma_endoptFLOAT0.00–1000—
nag_ref_boostoptFLOAT1.000–1000—
nag_ref_boost_aoptFLOAT1.000–1000—
nag_fit_modeoptCOMBOfit2 options: fit, crop (legacy)
nag_ref_boost_maskoptMASK—
nag_auto_phioptBOOLEANfalseDerive phi from alpha so alpha*phi keeps the upstream default linear guidance strength (0.25*4 = 1.0).
nag_phi_scaleoptFLOAT1.000–4Multiplier for auto phi. 1.0 keeps upstream-default linear guidance strength; higher/lower scales it.
vae_damage_correctionoptBOOLEANfalseSubtract estimated VAE damage after decoding, using the selected VAE's encoder and decoder for one extra round trip per image or face crop. The 2x VAE filters back to the current image size before subtraction. No original reference is needed.
vae_damage_strengthoptFLOAT1.000–40 skips correction; 1 is one-pass VAE damage subtraction. Values above 1 strengthen the same correction and can amplify artifacts. Does not add more iterations.
nag_alpha_scheduleoptCOMBOconstantNAG alpha over this stage's executed denoising steps. The global Settings / Configuration panel mirrors the same curve to every NAG stage; each stage spans its own effective step range.
nag_alpha_startoptFLOAT0.250–1First executed step's NAG alpha for the shared dynamic schedule.
nag_alpha_endoptFLOAT0.250–1Last executed step's NAG alpha for the shared dynamic schedule.
face_prompts_jsonoptSTRING[]—
require_single_faceoptBOOLEANfalse—

Outputs (6)

NameTypeDescription
imageIMAGE—
cropped_refinedIMAGE—
cropped_enhanced_alphaIMAGE—
maskMASK—
detailer_pipeDETAILER_PIPE—
cnet_imagesIMAGE—