Skip to content

Character Photo Auditor V2.0.0 — Handbook

The shared photo-intake step in front of every character creator: a person’s photographs (dropped into the app, or a zip) in, and out the training set a character creator reads, with one manifest any creator can read. RunPod only: an app on the pod, and the same node as a ComfyUI App Mode workflow.


0. Read this first

The one thing that makes this worth building

“Do my angles cover the range?” is the question that decides whether your LoRA can render a profile, and it is normally answered by impression. This tool answers it with a yaw histogram. It can say “zero frames beyond 55 degrees: the profile bucket is empty” before you spend GPU time discovering it.

That failure mode is documented, not theoretical:

DreamBooth3D (ICCV 2023): “DreamBooth tends to overfit to the subject views present in the training views”, front-facing collapse when “input images do not contain any viewpoint variations.”

ID-Booth (arXiv 2504.07392) quantified it on human faces at yaw σ = 26.12, with training data that was already a semicircle of angles. Pose collapse happens even with multi-view data.

One venue, two doors (V2.0.0)

Everything runs on the pod you rent (Paul, 2026-10-17: no local model on the Mac, the auditor runs on RunPod). Two doors open onto the same node:

  • The app (the easy way): https://<pod>-8188.proxy.runpod.net/apps/photo-auditor/, or the “Photo Auditor App” port (8190). Drop photos, a folder or a zip; name the character; tick the permission box; press Audit my photos. Every photo comes back as a tile with its verdict, the gaps say what to shoot next, and the training set downloads or waits in ComfyUI’s input folder for the creator. SoulCraft-branded, served by ComfyUI-App-Front, the brand-neutral pack this package vendors (§1).
  • The ComfyUI form: SoulCraft Photo Auditor.json in App Mode. Upload a zip (a 📇 Character sheet from Krea 2 Image Creator is already in the list), type the character name, pick the creator, press Run.

Both write input/<name>-audited.zip for the Krea 2 Character Creator (the creator’s adapter decides the name, the layout and the size, §3) and output/photo-auditor/<name>_NN/ with everything else, including manifest.json. The captions come from Qwen3.8-27B loaded in the same graph, through core ComfyUI’s own generate path (§2, the model and why).

Privacy, plainly. Your photos go only to the pod you rent; nothing is sent to any hosted API, and none of the package’s Python names a URL (a test holds it). A zip is opened in your browser and its images uploaded one by one to the pod; the result page’s thumbnails are your own files, still in the page, because the server sends no pixel back. The report is numbers and prose, never pixels: no thumbnails, no contact sheet, no embeddings written out. Identity vectors are computed in memory for duplicate detection and subject selection and discarded: an ArcFace embedding is a biometric template in its own right. The report closes with an assurance that it holds no pixels, and that sentence is produced by a check on the records, not typed into the template. The manifest holds the captions and the photos’ names inside the set (like the .txt sidecars it mirrors) and stays on the pod.

Design rules

  1. Originals are opened read-only. Never modified, moved, or copied in place. A bug in this code cannot damage irreplaceable photographs. All preparation goes to a separate staged folder; the app’s uploads are copied into the node’s scratch folder before anything reads them.
  2. No pixel edits to anything that gets trained. See §3. The staged copy is resampled ONCE, to the exact size the creator’s own loader would resample it to, so the loader resamples nothing. Measurement copies (a greyscale thumbnail for blur, a face crop, the buffer handed to the captioner) are not staged and are named individually in §3.
  3. The creator’s rules are the creator’s. Sizes, the zip layout, the caption grammar and the trigger rule are read from each creator’s own loader and live in its adapter (photo_auditor/adapters.py); a test proves the Krea 2 frames are idempotent under its _fit.
  4. Every threshold is a judgement, or it names its source. §5’s Sources table carries the quotes; §6 keeps the list of what remains a judgement.
  5. Verify against installed source, never README. Notably face.pose, verified, §6, and the captioner’s chat template, read from ComfyUI’s own tokenizer (§2).

1. Quick start

Install (once per pod; the base first, then this package):

Terminal window
uv run "base/comfyui-base/_build/pod/podctl.py" install <pod> --base --pkg "photo-auditor"

The installer fetches two model rows (Qwen3.8-27B, 17.3 GB, and buffalo_l), installs the node pack, mirrors ComfyUI-App-Front into custom_nodes/, copies the app to <user>/default/apps/photo-auditor/ and prints the app’s address in a box. bash "photo-auditor-script.sh" app copies the app again without a restart (App Front reads it from disk on every request).

The app (/apps/photo-auditor/):

step what you do
1 · Your photos drop photos, a folder or a zip on the zone (or Choose photos / a zip / a folder). The page opens a zip in the browser and keeps only images; junk (__MACOSX, ._ twins) and non-photos are set aside with the reason
2 · Who is it? the Name (for the report and the manifest), the trigger word (suggested from the name, checked live against the shared rule), the class word chips, and “Train for”: the built creators
3 · Permission “These are photos of me, or I have this person’s permission to train a character on their likeness.” Required
More options holdouts (5) and the smallest frame (512 px), the same dials as the form
Audit my photos each photo uploads on its own (App Front’s api/upload, at most 90 MB each, owner-tagged), then the audit runs; the page follows its stages and progress on ComfyUI’s websocket

The result page shows the verdict, the counts, every photo with its verdict (training rank, holdout, set aside and why, no face found, duplicate), the gaps and what to shoot next, Download the training set, what to do next in the creator, the full report, and Delete my photos from this pod (the run, its report and manifest, and every photo the page uploaded; the training set stays for the creator). My audits lists this browser’s own runs (a random key in the browser, sent in a header, never a URL).

The ComfyUI form (SoulCraft Photo Auditor.json):

field what it is
⬆ (on the zip row) your own zip through the row’s own upload control, or drop it onto the field; a 📇 Character sheet is already in the list
Your photos (zip) the zip to audit
Character name the trigger word: lowercase, 3 to 24 characters, unusual (vesperq, not paul). Names the zip and starts every caption; type the same name in the creator
Class word person (default), man, woman: the noun after the name. Never inferred
Character creator the creator the set is for: its zip layout and training size (today the Krea 2 Character Creator)
Captions Qwen3.8-27B (in ComfyUI) sees each photo and writes two sentences; generic writes the measured words only, in seconds, and loads no model
Smallest frame (px) the floor on the short edge of the size a frame trains at (512)
Holdouts likeness references, never trained on (5; never more than a third of the set)

Press Run. The panel shows the report and the manifest. Then open the Character Creator, refresh its page once (its zip list is built when it opens), pick <name>-audited.zip, type the same name, set Steps to what the report suggests, Run.

On the pod’s terminal, the engine without a vision model (the headless CLI, measured words only):

Terminal window
python3 cli.py ~/photos astrx7 --stage ~/photos-ready --name "Ada Lin"

It writes the same staged folder, zip and manifest as the node, with the measured-words captions. Exit code is meaningful: 0 clean · 1 the set has gaps · 2 usage. The pod script has the six command forms of every package here (no arguments, --check, --latest, test [tier], rescue, help) plus app and e2e <pod> (§9).


2. The three tiers

Tier Needs What it answers
1 deterministic nothing Is anything unusable? Size as trained, exposure, blur, format, EXIF, bit depth, what made it
2 geometric InsightFace buffalo_l, CPU Do my angles and framings cover the range? Is every frame the same person? Plus duplicates
3 the vision model Qwen3.8-27B in the same graph, on the pod’s GPU What the photo SHOWS: sunglasses, a screenshot, closed eyes, the outfit, the setting, the light, and the caption

Tiers 1 and 2 are CPU work on the pod: on 40 synthetic 12 MP frames an M4 measured tier 1 3 s and tiers 1+2 13 s (V0.3.0); a pod’s CPU is in the same range. Tier 3 is the GPU’s: its per-photo time on a B200 is not measured yet (§6); the app prints its own estimate as the review runs, and a re-run after a reshoot costs almost nothing, because reviews are cached by the photo’s content, the model and the prompt version.

The model, and why (V2.0.0)

Qwen3.8-27B (Apache-2.0, Qwen/Qwen3.8-27B, 2026-08-05, Qwen3_5ForConditionalGeneration, image-text-to-text), in Comfy-Org’s single-file repack Comfy-Org/Qwen3.8-27B @ 15daf09d, text_encoders/qwen3.8_27b_w4a8.safetensors, 17,286,101,500 bytes (the vision tower and the multi-token-prediction head included). Chosen from primary sources only (model cards, ComfyUI’s source at the v0.41.0 tag, the Hub’s API), for what the auditor must judge: whether a face is covered, the eyes, a hand, a screenshot, the clothes and the light, and two plain sentences of caption, returned as strict JSON.

model licence core ComfyUI loads it size Comfy-Org single file MMStar / HallusionBench / RealWorldQA
Qwen3-VL-8B (V1.x) Apache-2.0 yes 17.5 GB bf16 yes 70.9 / 61.1 / 71.5
Qwen3.8-27B Apache-2.0 yes, TEModel.QWEN35_27B 17.3 GB w4a8 w4a8 about 81 (27B family) / 70.0 (Qwen3.5-27B) / 85.9
Qwen3.5-9B (the fallback) Apache-2.0 yes 19.3 GB bf16 bf16 79.7 / 69.3 / 80.3
Qwen3.6-27B, Qwen3.5-27B Apache-2.0 yes 55.6 GB none 81.4, 81.0 / n/r, 70.0 / 84.1, 83.7
Gemma 4 31B Apache-2.0 yes 62.6 GB none 77.3 / n/r / 72.3
Qwen3-VL-32B, 30B-A3B, 235B; InternVL 3.5; GLM-4.6V; MiniCPM-V 4.5; Llama 4 various no none
  • Core loads it. comfy/sd.py at v0.41.0 detects the 27B and builds comfy.text_encoders.qwen35 (vision tower, generate, MTP decoding); comfy/quant_ops.py at v0.41.0 carries the AsymW4A8Int8Layout the file uses. So the package still has no third-party pack, and COMFY_MIN is 0.41.0.
  • Why it, and not a 32B Qwen3-VL: core’s only 32B Qwen3-VL entry is MiniMax H3’s cut 50-layer encoder, a full 32B would be detected as the 8B, and core has no detection for any mixture-of-experts vision model.
  • The caveats, said plainly: the cards score thinking mode and the auditor runs with thinking off; Comfy-Org publishes no quality figures for its w4a8 quantisation. The release’s GPU smoke compares it with the 8B’s known answers on labelled frames (sunglasses, eyes, a hand); if it loses, the fallback row (Qwen3.5-9B bf16) replaces it. It is never a switch on the form.

The chat template is passed, never left to the tokenizer. ComfyUI PR #17121 (merged 2026-10-17, after v0.41.0) found that on v0.41.0 thinking=False appended an empty think block to Qwen3-VL prompts, which the Instruct template does not have and which made Qwen3-VL-4B “reason inside its answers and hallucinate images”, and the wrong whitespace for the Qwen3.5/3.8 family. The node hands tokenize a llama_template that is the family’s official non-thinking image template, with thinking=True: v0.41.0 then appends nothing, and newer releases use a passed template verbatim, so the model reads the same text on both (review.TEMPLATES, held by a test). V1.x’s 8B captions ran under the wrong template on v0.41.0.

buffalo_l costs three different numbers, and quoting one of them is how an old figure got it wrong in both directions:

download 275.3 MiB (288,621,354 bytes)
extracted 325.5 MiB (five .onnx files)
on disk (a machine that keeps the zip) 600.7 MiB, because insightface never deletes the zip

On the pod the zip is a model row in the SHARED models/insightface/ folder (the Krea 2 Character Creator’s dest, one download for both), unzipped by the node on first use.

Tier 1 — the trainer’s size, not the file’s

Blur is measured on a common 1024 px greyscale copy so the numbers mean the same thing across mixed resolutions; clipping is the exception, per channel at full resolution (a LANCZOS thumbnail blends scattered clipped pixels away entirely — measured 0.0000 against a true 0.10).

The size floor is on the size the frame TRAINS at. The trainer’s loader scales every photo so its long edge is at most 1024 and never upscales, so a 12 MP photograph trains at 1024×768 and a 1000×750 photograph at 992×752 — within a few pixels of each other. V0.3.x put a 1024 floor on the original’s short edge and lost 88 of 118 real frames to it while accepting frames that trained at the same size. Since V1.0.0 the floor is 512 on the short edge of the trained size (§5 Sources: Krea 2 pretrained at 256/512/1024, Ostris trains at [512, 768, 1024]), and a frame that trains below the 1024 long edge is warned, never upscaled.

Rejects: under the floor; unreadable files; images in a mode that would need a colour or tonal edit to stage (§3) — including 16- and 32-bit modes, because Pillow converts them by clipping (measured on 12.3: 99.6 % of a 16-bit ramp becomes 255; a 16-bit TIFF would stage as a white frame). Warnings: soft frames (judged face-box against face-box and whole-frame against whole-frame — two measurements, two medians), blown highlights, crushed shadows, very dark or very bright frames, EXIF GPS, frames that train below full resolution.

What happens to your file formats

suffix read? what you get
.jpg .jpeg .png .webp yes measured, then staged as PNG at the trainer’s size
.avif .heic .heif .tif .tiff .bmp yes the same — the census notes the trainer’s loader could not have read the original
.cr2 .nef .arw .dng and other camera raw no listed under NOT READ; export a JPEG or PNG first
.gif .svg .psd .pdf .ico no listed under NOT READ with the reason
anything else no listed under NOT READ as “not a format this tool reads”

Nothing is ever skipped silently. Every file under the folder is either measured or counted in NOT READ with a reason; not read N is printed even when N is zero. The FORMATS census groups rejections by rule and shows one verbatim — V0.3.x counted identical strings, so eighty-two size rejections read as “1 of 82”.

Tier 2 — the tier that decides the result

  • Head pose (yaw/pitch/roll) → the yaw histogram, bucketed profile-L / ¾-L / frontal / ¾-R / profile-R, and a pitch histogram (looking up / level / looking down).
  • Face-box area as a fraction of frame → an objective framing histogram.
  • Who is the subject. Tier 2 starts from the largest face in each frame — and then, once every frame is measured, finds the set’s identity centre (the medoid of the single-face frames) and re-picks the face closest to it in every frame with more than one face, when that face clears cosine 0.35 and beats the largest one. Every frame gets identity_cos; under 0.30 it is listed under CHECK IDENTITY, ranked down and never used as a holdout — but never rejected, because ArcFace is weakest at exactly the profiles this tool exists to count. V0.3.x took the largest face, full stop, so a friend nearer the camera was measured, counted in the histograms and — with the highest detection score — preferred as a likeness reference.
  • Identity-embedding duplicate clustering at cosine ≥ 0.94 → burst frames that differ in pixels while carrying no new information. The sharper frame of each chain survives; a frame that is the only one in its angle or framing bucket is never dropped as a duplicate.
  • An identity-coherence line, the mean cosine to the set’s centre, warned under 0.55.

The detector is SCRFD-10G (det_10g.onnx), published WIDER FACE 83.05 on the hard set. It rescales the whole frame into a fixed canvas first, so a face 200 px across in a 4032 px photo becomes 32 px at the default 640 canvas. Measured: a 40 px face in a 4032×3024 frame is missed at 640 and found at 1600, with a yaw within 1.2° of truth — so a miss triggers one retry at 1600. Lowering the detection threshold recovered nothing (the face is never proposed at all), so there is no threshold rung. Frames still undetected become unmeasured: kept, excluded from every histogram, staged to their own folder, never captioned.

face.normed_embedding is used throughout, with a normalising fallback: cosine similarity is only a similarity between unit vectors.

Tier 3 — the tier that decides whether a photograph can be used at all

Prompt v3 → v4 (2026-09-08). The first pod pass planted a face behind mirrored sunglasses and Qwen3-VL-8B answered face_blocked_by: "nothing", eyes: "open". v3 added sunglasses as a yes/no — and the model answered false. The diagnosis, run on the pod against the same frame: the prompt closed with a worked example whose values were exactly "nothing", false, "open", and a greedy decoder copies a worked example; repetition penalty, sampling and a face crop changed nothing. v4 shows the keys with type placeholders and no values, and the same model, frame and decoding answer sunglasses: true, eyes: "not visible" — and face_blocked_by: "glasses" for the clear spectacles in the next frame, a better observation than v3 made. Each version bump retires every cached review.

Prompt v5 (V2.0.0) keeps v4’s rule (keys and types, never example values) and adds one observed key, setting ("indoor", "outdoor" or "studio"), a meta-tag the manifest carries; any other answer is dropped, never guessed into one of the three. The version is part of the cache key, so v4’s answers retire.

The captioner is Qwen3.8-27B loaded by the graph’s CLIPLoader (type stable_diffusion, which builds core’s Qwen3.5-family text model, the one with generate and the vision tower). The node drives it through core’s own tokenize → generate → decode, one load for every photograph, greedy decoding, thinking off through the explicit template (§2). Same prompt, same parser, same judge, whatever the model.

The model observes; the code judges. Measured on qwen3-vl:8b: asked for a verdict, it reported “face partially obscured by black bar” and set usable to true, three runs out of three. Models of this size fill fields independently rather than applying a policy. So the prompt asks only what the model can see, and the criteria live in review.judge(), explicit and tested without a model. Prompt version 2 (V1.0.0) no longer asks for framing or camera angle — tier 2 measures them — and adds eyes (closed ranks down, never rejects), expression (a variety axis) and hand_near_face (a flag). The prose is two sentences: the Krea 2 technical report says the model was trained “predominantly on long captions”.

A frame is rejected when: the face is not clearly visible; it is covered by sunglasses, a hand, a mask, a phone or similar; it is not a photograph (screenshot, screen, print, render); an edit alters the face itself; or someone else is the main subject. Flagged but kept: ordinary glasses, more than one person, closed eyes, a hand near the face. A rejection is never a deletion — rejected frames go to rejected/ with a .txt saying why. A model failure never rejects anything.

One captioner, no switch. The brand is SFW only, so the form has no field that swaps the captioner for another model: the stock Qwen3.8-27B is the only one, and the generic captions are the only alternative (they load no model at all). The app always uses the model.

Captions are composed at STAGING, not by the model: "<name>, a person, <framing>, <angle>[, looking up]. <the model's two sentences>". The trigger leads, the class noun follows (DreamBooth: “a unique identifier followed by the class name”), the framing and angle words come from tier 2’s measurements, and the prose is the model’s. V0.3.x composed the caption inside the vision tier and cached it with the trigger baked in, so a re-run with a new trigger served the old one; composing at staging makes that impossible. The Character Creator’s loader keeps a caption verbatim when it contains the name.

Variety. Clothing, background, lighting and expression are counted across the set, floor 2 each (§5 Sources). Hair is observed and reported, not required to vary: Replicate’s Flux trainer README says “avoid different haircuts or ages” in one set.


3. What it prepares, and what it refuses

Prepares one zip for the chosen creator, one manifest for every creator, and folders for you:

<dest>/<name>-audited.zip train/ + holdout/ + manifest.json: exactly what the Krea 2 Character
Creator's loader reads (it reads image suffixes and same-stem .txt only, so
manifest.json changes nothing there). Copied into ComfyUI's input/ on the
pod. Never named <name>-sheet.zip, which would overwrite the Image Creator's
character sheet for that name.
<dest>/manifest.json the character manifest (below), also at the zip's root.
<dest>/train/ 001.png, 002.png, ... each with its 001.txt caption, NUMBERED IN RATING ORDER
<dest>/holdout/ the likeness references the trainer never trains on. No captions.
<dest>/unmeasured/ frames where no face was found. Look at them. Never captioned.
<dest>/rejected/ what the vision review turned down, each with a .txt saying why.
<dest>/README.txt what each folder is, and the Steps to set in the trainer.
<dest>/MANIFEST.tsv every new filename joined back to your original.

Every image is written at exactly the size the trainer’s loader will use, as PNG, with all EXIF including GPS removed and the orientation baked into the pixels. The loader (krea2_character_creator/nodes.py, _fit) scales the long edge to 1024, never up, never cropped, rounding each side down to 16: a 4032×3024 phone photo trains at 1024×768, a 6000×4000 frame at 1024×672, a 1000×750 frame at 992×752. Staging at that size means the loader resamples nothing — a test extracts the loader’s own _fit and proves every staged size is a fixed point of it. V0.3.x mirrored ai-toolkit’s pixel budget instead (1176×888 for the same phone photo), so every frame was resampled a second time on the pod. Nothing is ever upscaled, cropped or padded. A zip over ComfyUI’s default upload limit of 100 MB (--max-upload-size) is named as such in the README.

Originals are untouched throughout. Nothing is ever overwritten or deleted — staging into a folder that already holds files writes a new timestamped folder beside it; on the pod each run gets output/photo-auditor/<name>_NN/.

The manifest: one record of the character, for every creator (V2.0.0)

manifest.json, schema photo-auditor/character version 1 (photo_auditor/manifest.py): the character (name as a person reads it, the trigger, the class_word), the tool (version, captioner, prompt version), the creator it was staged for (key, label, package, zip, size rule, next step), the verdict (ready, usable, not ready, decided once by engine.verdict_of), the summary counts, the gaps, the shot list, the variety counts, the notes, the census of files not read, and one entry per photograph:

key what
id, source, file a stable id (p001 …: training frames by rank, then the rest by name), the name inside the set, the staged file
split exactly one of train, holdout, rejected, unmeasured, duplicate
rank, rank_reason 1..n over the training frames, and why each ranks where it does
reasons, warnings, duplicate_of why it was set aside, what to look at, which frame it repeats
measured yaw, pitch, roll, their buckets, framing, face fraction, sharpness, identity (scalars only)
observed what the vision model saw: clothing, background, hair, lighting, expression, setting, eyes, glasses, a hand near the face
caption_raw, caption the model’s two sentences, and the caption staging wrote (identical to the .txt)
generator what made the frame, when its own metadata says so (a ComfyUI PNG’s model files, the EXIF Software tag)

It never holds pixels, embeddings or an absolute path (a test scans it); it holds the captions and the names inside the set, like the .txt sidecars, and stays on the pod. A field is added in a later version; none is renamed or removed without a new version number.

Adapters: one per character creator (V2.0.0)

photo_auditor/adapters.py. An adapter is a creator’s hand-over: its zip name, the size its loader trains at (size_for), the folders that travel, the caption extension, and refuses_generated_by, the generators whose OUTPUTS that creator’s licence forbids training on (a frame whose own metadata names one is refused right after tier 1, before anything ranks or holds it out, with the reason in the report). Built: Krea 2 (<name>-audited.zip, long edge 1024, stride 16). Planned, documented in adapters.PLANNED and never offered in a form: MiniMax H3 (ai-toolkit in I2V mode per its successor plan; size and captions not decided), LTX 2.5 (nothing recorded yet), FLUX 3 Klein. The brand rule bars FLUX 3 Klein outputs from feeding any trainer; auditing real photographs for a Klein creator is not barred by that rule, so its adapter will refuse Klein-generated frames by refuses_generated_by, and whether a Krea 2 sheet may train a Klein character is that creator’s licence question. A new creator is a new Adapter and a line in the node’s combo, never a change to the audit.

Refuses, on the Krea 2 handbook’s own evidence — “never pre-crop, pad or upscale”:

Asked for Why not
Crop / pad / upscale Destroys framing variety the model needs; padding is learned as content; the trainer never upscales and neither does this
Sharpen / denoise Teaches the LoRA the filter’s texture signature
Exposure or colour normalisation Erases the lighting variety the LoRA specifically needs
Deblur Blur is a signal to look at the frame, not a repair target
Flattening an alpha channel Pillow discards alpha rather than compositing it. Flatten it yourself and re-run
Converting CMYK or a palette A correct conversion is an ImageCms transform, which is a colour edit
Reducing 16- or 32-bit depth A tonal choice (Pillow would clip to white); export an 8-bit file yourself
Rewriting a mismatched ICC profile Passed through when the mode is unchanged, dropped when a conversion changed it

The three measurement copies that are not staged: the 1024 px greyscale thumbnail used for blur, the 256 px face crop used for face-box blur, and the 1280 px buffer handed to the captioner (the vision model tiles and resizes internally regardless, and its vision tower runs in fp32 with no memory estimate). None reaches disk; none is seen by the trainer. These refusals are documented inline in engine.py as well as here, and the suite walks the syntax tree of every module to prove no forbidden call exists outside the named functions.


4. Held-out selection

Incoming references are kept. A zip that already carries a holdout/ folder — the Image Creator’s character sheet ships four — keeps those frames as references: they are measured and reviewed like any other (an unusable one is dropped), they count toward the Holdouts field, and the auditor only tops the set up. A zip that is an earlier audit’s output carries rejected/ and unmeasured/; the scan skips those folders as the trainer’s loader does, and the report counts them under NOT READ.

The pick spreads. Phase two of the selection walks every angle bucket with spare frames round-robin, largest surplus first — the first pod pass (17 frontal frames of 28) held out four frontal references of five because the fullest bucket won every round, which is the clustered anchor holdouts exist to avoid.

The tool picks holdouts (default 5) references that span the yaw and framing range rather than five frames from one burst. The Character Creator’s likeness test anchors on the mean of these embeddings, so a clustered holdout set would measure likeness against one angle and call it identity.

Three phases, and the order is the rule. A bucket donates a reference only while it keeps 3 frames for the trainer; then the buckets with the most to spare; only as a last resort a thin bucket — and then the report says HELD OUT: 'profile L' has 1 usable but 0 reach the trainer. V0.3.x took the best frame of every bucket in turn, so the set’s only left profile could become a reference the trainer never saw, while the gap report (which counts holdouts) called the bucket fine. An identity outlier is never a holdout: one wrong person in the anchor’s mean bends every score. Never more than a third of the usable set is reserved.

If tier 2 is unavailable there is no pose to span, so holdouts are chosen for diversity across aspect ratio, megapixels and sharpness instead, and the report labels them “selected without pose”.


5. Reading the gap report, and where the floors come from

Gaps are stated as EMPTY, THIN, COUNT, SPREAD, UNMEASURED or HELD OUT, in plain language, with the reason attached. Then SHOOT NEXT turns them into photographs to go and take.

Blur no longer drops anything. It is measured inside the face box on a crop resampled to a common 256 px; frames with no face box fall back to whole-frame blur, and the two kinds are judged against their own medians. --blur-reject 60 restores an absolute floor.

Every threshold in force is printed in the report itself, with the version and a UTC timestamp, and --compare PREVIOUS.json prints what moved between two runs.

Sources — the floors that name a primary source, and the quote

All read on the cited page on 2026-09-07; the handbook quotes the vendor, never a guide site.

Floor Value Source
Usable count blocking under 10, thin under 12, more than needed over 60 Replicate’s Flux fine-tuning docs: “at least 10 images”; the Ostris trainer README on Replicate: “Aim for a dataset of 12-18 images”; Krea’s LoRA docs: “quality matters far more than quantity”
Training steps (advisory line in the report) 300–500 / 500–800 / 800–1500 by image count Krea’s LoRA training docs: “5-10 images: 300-500 steps; 15-30 images: 500-800 steps; 50+ images: 800-1500 steps”
Variety axes clothing, background, lighting, expression — 2 each Replicate’s Flux trainer README: “different settings, facial expressions, and backgrounds”; Krea: “different angles, expressions, lighting, and backgrounds”; BFL’s klein guide: “Mix close-ups with full scenes”, “Avoid repetitive backgrounds”; the Character Creator handbook: two lighting setups, two backgrounds
Hair observed, not required to vary Replicate’s Flux trainer README: “avoid different haircuts or ages”
Hands a flag, and a note above 25 % of frames the Ostris trainer README on Replicate: avoid “showing hands in a lot of face framing positions”
Size floor 512 on the trained short edge; a warning under the 1024 long edge Ostris ai-toolkit README: “Images are never upscaled” (verified; no minimum stated anywhere); the Krea 2 technical report: pretraining “spans 256px, 512px, and 1024px”; Ostris’s Flux and Qwen-Image configs: resolution: [512, 768, 1024]; fal’s Krea 2 trainer default 768
Trigger not a real word, not tok/sks replicate train.py: “Pick a string that isn’t a real word”; Replicate’s docs: “TOK” collides with other fine-tunes; DreamBooth: rare tokens
Caption grammar trigger first, class noun, then long prose DreamBooth: “a unique identifier followed by the class name”; kohya’s keep_tokens; the Krea 2 technical report: trained “predominantly on long captions”; BFL: “Describe everything visible except the … concept”; fal on Krea 2: name “the changing details, and not the features”
Krea 2 train on RAW, stride 16 krea-ai/krea-2 README: “Train LoRAs on RAW and apply them on Turbo”; Ostris krea2.py: divisibility 16

The report sections

VERDICT opens the report — READY, USABLE, with N thin spots, or NOT READY — with the train/holdout split and the suggested Steps. FORMATS and NOT READ are the censuses. IDENTITY is new in V1.0.0: the coherence line, how many frames had the subject re-selected, and CHECK IDENTITY — the frames a human must look at. TRAINING GEOMETRY shows the size each frame trains at, citing the loader’s rule. FRAME SIMILARITY prints the pair-similarity distribution so the 0.94 duplicate cut can be judged rather than trusted. RANKED BY WHAT EACH FRAME ADDS orders the usable frames by scarcity first, sharpness second — a soft, closed-eyes profile still beats the ninth frontal bust shot; frames that train below full resolution, closed-eyes frames and identity outliers drop first among equals.


6. What is NOT proven — stated so nothing here reads as settled

face.pose is VERIFIED against the installed package: 1k3d68.onnx loads as landmark_3d_68 with require_pose True; all 6/6 faces in insightface’s own test image carry a finite 3-vector pose; the PIL load path is byte-identical to insightface’s own cv2 path (cosine 1.000000); mirroring negates yaw to within 3.44° worst case; positive yaw means the subject is turned toward the viewer’s right.

Not proven in V2.0.0, said plainly (nothing ran on the Mac, by rule):

  • Qwen3.8-27B’s answers against the 8B’s. The choice rests on model cards and ComfyUI’s source; the release’s GPU smoke (HF ZeroGPU) is what compares it with the 8B’s known answers on the labelled frames, and §9 records the result when it has run. Until then the fallback (Qwen3.5-9B bf16) stands ready.
  • Its time per photo on a B200, and whether the pod’s PyTorch build has the fast w4a8 kernels (comfy/quant_ops.py names a cu130 floor). The app estimates from the run itself.
  • The app on a pod. Its whole flow (photos, a zip, the result, Delete) runs on the CI runner’s CPU testbed with generic captions; a RunPod pass happens only with Paul’s yes and its cost stated first.
  • The zip reader in Safari and Firefox. It runs on Chromium (the App-view tier) and Node (the app tier); DecompressionStream("deflate-raw") is in both other engines’ current releases, untested here.
  • HEIC on the pod. pillow-heif==1.8.1 is in PIP_EXTRA and the import check; no HEIC frame has been audited on a pod yet.

Still not proven, or a judgement:

  • Detection recall at the tails is the real open question: the escalation ladder was measured on one synthetic arrangement of one face.
  • The framing bin edges are uncalibrated (the first edge sits at 0.010; a plausible full-body frame lands near 0.008–0.011).
  • Blur thresholds are advisory; the duplicate cosine of 0.94 is a judgement.
  • The identity thresholds are judgements: re-select at 0.35, check under 0.30, coherence under 0.55. ArcFace cosines between different people sit near 0 and rarely pass 0.3; the same person across angles lands 0.4–0.9; a strong profile can score under 0.30 and is flagged, not rejected.
  • The base’s canvas kit was fixed on 2026-09-08 (2.0.38) and its tests run here again. Under this suite’s warnings-as-errors, five of them had failed on unclosed file handles, and the base’s --check gate then refused to install this package at all; the package carried closed-file copies and a shadowed fixture until the base landed the fix. Those four copies remain as package-tier tests: they check the shipped zip against the tree, the gates on every tier test, and the handbook’s own counts, which is worth keeping whoever owns the kit.
  • The size floor of 512 and the exposure limits (mean luma 40 / 215) are judgements informed by the sources in §5, not published optima.
  • Four older numbers are judgements too: the 1600 px escalation canvas, the 256 px face-blur window, the 1280 px captioner buffer, the 3-per-bucket, 3-full-body and 1-close-up floors.
  • HEIF metadata is not exhaustively inspected. The GPS check reads the EXIF block Pillow exposes; the report says how many images were found to carry GPS, and that zero is not proof.
  • No likeness is evaluated here. The Character Creator’s proof sheet is the judge.
  • InsightFace licensing: the code is MIT but the model weights are non-commercial research use only. Fine as an internal quality gate; do not redistribute them.

7. Record

version date what
V0.1.0 2026-09-06 First cut. auditor.py (tiers 1–2, gaps, holdouts, staging, report) and cli.py (tier 3 via Ollama, CLI). Never executed.
V0.2.0 2026-09-06 First execution, and the two defects it found that would have produced a wrong answer: tier 2 read every image without applying its EXIF orientation, so iPhone portraits reached the detector sideways; and records were keyed by basename while discovery recurses. Plus ~28 further fixes, the shell wrapper, and a 157-test suite.
V0.3.0 2026-09-07 Made fast, and a file may no longer vanish. Discovery had dropped unlisted suffixes silently; .avif is read; FORMATS and NOT READ censuses. The review became rank-ordered, capped at the best-ranked N (a flag of the Mac CLI, gone in V2.0.0), cached on file content — ~62 min believed → ~5.2 min measured → ~0 s on a re-run. Concurrency measured and rejected.
V0.3.1 2026-09-07 The alignment port. insightface 1.0.1 still calls scikit-image’s deprecated SimilarityTransform.estimate; the one function is replaced with a port on from_estimate, proven bit-identical.
V0.3.2 2026-09-07 Latest, and nothing filtered. Python 3.14; every warning an error in the suite; the wrapper rebuilds a venv on the wrong interpreter by itself.
V1.0.0 2026-09-07 The pod venue, and the trainer’s own geometry. A ComfyUI Base package: one own node (ComfyUI-Photo-Auditor), one App Mode workflow, Qwen3-VL-8B as the captioner through core’s own generate path, a 🔥 Uncensored toggle over a decensored 8B (heretic-org’s Heretic run, repacked from four shards by repack.py), the face pack as a model row in the pack’s folder. The Mac wrapper became Local.sh on the same engine. The repacked 🔥 file’s sha256 is pinned (0f669cba8653…, measured on the pod 2026-09-08) and --check verifies it. What the review found and fixed: staging mirrored ai-toolkit’s pixel budget while the live trainer’s loader caps the long edge at 1024 (every frame resampled twice); the 1024 short-edge floor rejected frames that trained at the size of accepted ones (88 of 118 real frames lost) — now 512 on the trained short edge; the review cache baked the trigger into captions — now composed at staging; 16-bit modes staged as white frames — refused by name; the blur median pooled two measurements; SHOOT NEXT repeated a line; the FORMATS census counted strings not rules; holdouts could drain a thin bucket — three-phase donation with a HELD OUT line; the largest face was always the subject — a second pass re-picks by identity and flags outliers. What the trainer vendors’ own guidance changed: count floors 10/12, variety axes with expression and without hair, a Steps line, the trigger note, long two-sentence captions, prompt v2 with a schema and thinking off. The dead JPEG path and the stale docs went.
V1.0.1 2026-09-08 Caught up with twelve releases of the base, and stopped littering a shared directory. BASE_MIN is now 2.0.40, and that is a measurement rather than a wish to be current: this package’s node packs import for over three minutes, so on any older base a perfectly healthy install of it reports restart FAILED and stops — the defect this package reported and 2.0.40 fixed. Base 2.0.38 closed the canvas kit’s files, so the workaround this suite carried is gone and two of its three closed-file copies went with it; the third stays because the kit’s version filters its expected member list by what exists, so a shipped file deleted from both the tree and the zip would pass unnoticed (reported). The live App Mode test used to leave its upload behind on every run — the panel’s own picker cannot post overwrite=true, so the server renamed each one and twenty had piled up in the testbed’s input/, which three sessions share and which is also the zip combo’s option list; the test now removes only what it added. Base 2.0.47 ships _build/verify.sh, which supersedes this package’s pre-upload gate for two of its three steps: _build/simulate-pod.sh stays as the second venue, because verify.sh runs the zip-suite in the base runner’s venv, which has no insightface.
V1.0.2 2026-09-08 The build id became diffable, and two implementations stopped disagreeing about a missing file. build_manifest() is now separate from build_tag() in both the node and the installer’s shell, because a digest can only say “different” while two manifests diff to the file that changed — the argument that settled the scheme with the MiniMax H3 session. Splitting it exposed a real divergence: on a file that is absent, Python wrote MISSING and the shell wrote an empty digest, so the two disagreed in exactly the case the tag exists to catch. Both now write MISSING, and a test compares the two manifests byte for byte and covers the empty-pack case. The LC_ALL=C sort pin is now asserted in the SOURCE, because no run of this suite can catch its absence: macOS ships BSD sort, which ignores the locale, so the manifests agree on this Mac with or without the pin and would diverge only on the pod’s GNU coreutils.
V1.0.3 2026-10-16 Its own repository. The package moved from the SoulCraft-Workflows monorepo to SoulCraft-Org/photo-auditor, cloned beside its siblings in the SoulCraft-Org/workspace checkout, with ComfyUI Base read from $COMFY_BASE. The suite looked for the base at <repo>/base/comfyui-base, which a workspace does not have: the BASE_MIN check then skipped without a word. It now reads the base the runner put on PYTHONPATH. The script’s dead monorepo base lookup is gone; the e2e driver reads the private cast from the workspace’s tools/cast/.
V1.0.4 2026-10-16 The suite collects again, and four tests caught up with their siblings. A collection failure had hidden five reds since the 2026-09-12 layout. The sibling-forms test now checks what the e2e driver types since its 2026-09-11 port (the Image Creator’s prompt, tier and photo; the Character Creator’s name, zip, photo and steps; no mode picker); the staging test reads the sheet saver where it lives now, the Character Creator’s pack; the test-character test reads the trigger from the private cast in the workspace’s tools/cast/, as the driver does, instead of the retired name key; the brand check allows only the shipped workflow’s own filename, which carries the brand by the naming rule; the live form test drives the server BASE_SERVER names (after CPA_APP_SERVER) instead of whatever answers on 8199, and skips when that server was not prepared for this package (the CI’s testbed); the test-character test skips where the private cast is absent (the CI). The ollama tier was not run on the Mac (a 25 GB load froze it once): owed on a machine with the memory (HANDOFF).
V1.1.0 2026-10-16 SFW only: one captioner, no switch. The brand rule (no field, toggle, flag or model row that turns nudity or sexual content on) applied to the captioner, the way the Video Creator’s V8.1.0 and the Klein Image Creator’s V1.1.0 applied it. Removed: the 🔥 Uncensored field and the node’s uncensored input, its second lazy CLIP input clip_uncensored, the second loader and its frame and note, the snapshot row heretic-org/Qwen-3-VL-8B-Instruct-heretic (Hub tags heretic, uncensored, decensored, abliterated), the repack hook and repack.py, the image-refusal stage of the test-character kit and its own subject, and the §2, §5 and §9 text about them. The form is six controls; the graph is 8 nodes (five notes), 2 links, 4 groups; two model rows, 17.8 GB. The stock Comfy-Org/Qwen3-VL qwen3vl_8b_bf16 is the only captioner. The old files are SUPERSEDED (the repacked single file, and the four shards inside the old snapshot folder, which is a LEGACY_DIRS entry because the base’s sweep matches basenames only inside a package’s own folders): offered for deletion, never taken. The snapshot’s small files (about 17 MB) stay where they are. New tests hold the rule: no model row, workflow string, node input, shipped source or handbook section outside this record names or enables such a model.
V1.1.1 2026-10-16 The Hub row is pinned to a commit. ComfyUI Base 3.12.0 honours the revision in a row and checks each Hub file against the sha256 the Hub states for that commit, once, at download, so a repo that moves or renames a file upstream can no longer 404 a row or change what installs (the H3 latent upscaler broke that way on 2026-09-17). The captioner’s row (Comfy-Org/Qwen3-VL) now names commit 02f0d3e, from models.py’s new REV table (the commit base.sh gen-models <dir> --pin printed on 2026-10-16); the file, its size and its Hub sha256 are unchanged. The InsightFace row is a GitHub release asset and stays as it is. Files already on a volume are kept as they are: no download and no re-hash. BASE_MIN is 3.12.0, because an older base drops the revision and fetches main. No graph change but the version stamp.
V1.1.2 2026-10-16 The zip row previews nothing, and Run sends no button. Two App Mode findings from the first form pass on frontend 1.55.17. The frontend’s own image-upload input, which it adds beside every image_upload combo, previewed the zip row’s value as a picture: /api/view for the none placeholder, twice, 404, and after an upload it would have fetched the whole zip back as a broken image. The pack drops that input from the node definition; the App panel’s upload control comes from the combo’s own flag and stays. The canvas button reached the queued prompt as "Upload photos (.zip)": null: the frontend leaves a widget out of the prompt only on options.serialize: false, which the button now sets beside serialize = false. The applive form probe now prepares the server itself (the workflow through ComfyUI’s userdata API, the encoder stand-in inside the gate’s testbed, both undone after), so it runs on CI; it uploads a small zip through the row’s own picker (the upload path survives the dropped input: the combo and the prompt carry the server’s name for it), presses the real Run with /prompt answered by the probe, and checks both findings live. Its toggle count reads 0 (SFW only since V1.1.0). One new static test; 233 test functions. Also: BASE_MIN 3.17.1 (the base marks the App Mode tour seen on install); the form driver calls the base’s coach_tour and stops when a tour opens on such a server; four tests whose checks need the development tree or a sibling package now SKIP when it is missing instead of passing without them, so CI’s VERIFY_SUITE=zip re-runs them from the repo (error log row 106).
V1.1.3 2026-10-17 Ollama on the Mac is capped. cli.review_one asks for num_ctx 8192 (OLLAMA_NUM_CTX) and keep_alive 1m. Ollama’s default context for qwen3-vl:8b is 131072, which loaded the model at 26 GB (14.8 GB resident) on the 24 GB MacBook Air and froze it during a local verify run. One review needs about 2.6k tokens (a 1280 px frame is at most 1600, the prompt about 800, the answer about 200), so 8192 is three times that and keeps the model near 6-7 GB; the minute of keep_alive holds it warm through a serial run and unloads it after. A degrade test pins both.
V2.0.0 2026-10-17 RunPod only, a SoulCraft app, one manifest for every creator (Paul, 2026-10-17). The Mac front door is gone: photo-auditor-local.sh, the Ollama tier of cli.py (which stays as the engine’s headless CLI, measured words only) and every “nothing leaves the Mac” promise; the privacy rule is now photos go only to the pod you rent; nothing is sent to any hosted API. The app: a SoulCraft-branded page at /apps/photo-auditor/ (and port 8190) on the vendored, brand-neutral ComfyUI-App-Front 1.10.1 (byte-identical to the Video Replacer’s): drop photos, a folder or a zip (opened in the browser, uploaded one photo per request), name, permission, Audit my photos, every photo’s verdict on its own thumbnail, the gaps, the download, Delete my photos. The captioner is Qwen3.8-27B (§2, the table and why), Comfy-Org’s w4a8 single file through core ComfyUI 0.41.0, with the chat template passed explicitly (ComfyUI PR #17121). Prompt v5 adds setting. manifest.json (schema photo-auditor/character 1) beside the report and in every creator zip; adapters per creator (Krea 2 built; MiniMax H3, LTX 2.5, FLUX 3 Klein planned, never offered), with refuses_generated_by from a frame’s own metadata. The node gains creator, photos and display_name, a progress bar and a counts-only stage event, and returns the manifest; HEIC via pillow-heif; the 8B file is SUPERSEDED. The engine venue moved from the Mac venv to CI’s engine job.

8. Running it

  • On the pod: podctl install <pod> --base --pkg "photo-auditor". The thin script declares two model rows (§2: Qwen3.8-27B and the face pack), PIP_EXTRA=( insightface==1.0.1 onnxruntime==1.29.0 pillow-heif==1.8.1 ), the first two the Character Creator’s pins (a vendored pack’s requirements are read only when the base builds a venv, which a warm pod never does; opencv is deliberately absent because two base packs already install it), VENDORED_PACKS=( "ComfyUI-App-Front" ) (the base mirrors it into custom_nodes/; on a pod shared with the Video Replacer the newest version wins, and one version with two contents is flagged), APP_PORTS=("8190|photo-auditor|Photo Auditor App"), GPU_PREFER="B200 B300" and COMFY_MIN="0.41.0", and three hooks: pkg_pre_models copies the node pack into custom_nodes/ and asks for the restart when the running server lacks this build of the node; pkg_import_check proves the imports in the pod’s venv; pkg_post_models copies the app to <user>/default/apps/photo-auditor/. The files V1.0.x’s second captioner and V1.x’s 8B left on a volume are SUPERSEDED: the base offers them for deletion (one y/N, default No) and never takes them. The review cache lives under user/default/photo-auditor/.
  • Warnings are fixed, never filtered: the suite runs with every warning as an error. One deprecation cannot be upgraded away, and it is ported: insightface 1.0.1 calls scikit-image’s deprecated SimilarityTransform.estimate on every embedding; engine._estimate_norm replaces the one function, proven bit-identical. The engine never downloads: on the pod only the base’s model rows do, and the node unzips the face pack on first use.
  • Never on the Mac (Paul, 2026-10-17): no suite, no testbed, no model runs there. Every pull request runs two venues on the free CI runners: the shared gate (zip matches tree, the suite from the repo and from the extracted zip on the base’s testbed with ComfyUI and its server, the App-view tiers that drive the ComfyUI form and the app through the DOM, the cast guard), and the engine job (the hash-pinned requirements.txt on Python 3.14 with buffalo_l from insightface’s own release asset, the whole suite: the insightface tiers run there). The release’s GPU smoke runs on HF ZeroGPU, capped.
  • Before an upload: bash _build/simulate-pod.sh runs the suite from the extracted zip, the tree the pod’s --check gate runs it in, on a CI runner or a cloud session, never the Mac.
  • The suite: photo-auditor-script.sh test [tier] through the base’s runner in the testbed venv, and .venv/bin/python -m pytest suite.py in the engine venue: the same suite.py, 245 test functions in 28 tiers (pytest.ini’s markers; uiux, snapshot and config are the base’s canvas kit, imported whole). Every negative test carries a positive control, and a tier that needs something a venue lacks (insightface, torch, the base, a ComfyUI checkout, a live server, node) skips and says so.

9. The test character — the end-to-end proof

The whole chain is proven on a person who does not exist: tstchr9, rendered by Krea 2 Image Creator from the recipe in _build/testchar/persona.json. bash "photo-auditor-script.sh" e2e <pod> <stage> drives every stage through the real App Mode forms (_build/testchar/e2e.py; a pass trains 150 steps a run by default to prove the chain, --full trains the 600-step comparison; Playwright over the DOM: the workflow opened from the server’s list by name, real fields, the real Run button): ① Create makes the identity render → 📇 Character sheet makes input/tstchr9-sheet.zip (24 training renders + 4 holdouts) → the exercisers (② Restage renders for sunglasses and closed eyes, a composited second persona nearer the camera, two downscaled and two duplicated sheet frames) make the “plus” zip → the Audit form on both → the Character Creator trains on the raw sheet and on the audited zip, and its own likeness test scores both. The recipe and the numbers are tracked; the renders never are.

Changed on 2026-09-11 — read the measurements below as the OLD kit’s. Two things moved. (a) Who the character is no longer lives in persona.json: that file holds ROLES now, and who fills them comes from the private test cast in base/comfyui-base/_build/cast/, so every package’s private tests exercise the same invented adult from photographs to video. (b) The sheet is the Character Creator’s, not the Image Creator’s: 📇 moved there in its V1.1.0, and a zip left on — none — plus one photo renders the 24 scenes and trains on them in a single Run — so the RAW arm of the comparison is that run, not a second training. The Image Creator’s form is five fields with no mode picker since its V2.0.0, so the driver types words and uploads photos and lets the router decide, as a person does.

What the pass measured (2026-09-08, pod 5lgll2pfr93u3j, RTX PRO 6000 Blackwell, ComfyUI 0.34.6)

stage through the form result
① Create reference Image Creator, ◆ Quality one 1024² studio portrait
📇 Character sheet Image Creator, Photo 1 = the reference 24 + 4 frames in 250 s, input/tstchr9-sheet.zip (40.7 MB)
Audit of the sheet this workflow, Qwen3-VL-8B captions 254 s cold, 92–96 s with the review cache warm; 28 of 28 usable, 0 rejected, 0 duplicates; report in the App view’s output area; input/tstchr9-audited.zip written (23 for training + 5 held out: the sheet’s own 4 kept, 1 chosen)
Audit of the plus zip uploaded through the zip row’s picker (55 MB) 209 s cold under v3, 350 s cold under v4 (36 frames reviewed); 36 scanned, 31 usable, 1 rejected by the size floor, 4 duplicates marked; the sheet re-audited straight after in 90 s, its frames already in the v4 cache

The exercisers, as the plus audit judged them (its audit.json, checked by the driver) — eight of eight pass on the v4 build:

planted expected measured
a 480 px copy rejected by the 512 floor rejected (“trains at 480x480: short edge 480px under the 512px floor”)
a 640 px copy warned, kept kept, “below the trainer’s 1024px long edge” — and marked a duplicate of its source
eyes closed flagged, ranked down, kept flagged (“vision flag: eyes closed”), ranked 27th of 31
two near-duplicates chained to one keeper both → kept train/001.png
a second persona composited larger in front re-selected by identity or flagged re-selected: “the largest face (cos 0.13 to the set’s centre) is not the subject”
② Restage “stranger nearer” (the same identity twice — see below) kept, “2 faces detected”, no identity warning — correct
mirrored sunglasses vision-rejected NOT rejected under prompts v2 and v3 (sunglasses: false, face_blocked_by: nothing, eyes: open, deterministic), while the same 8B through Ollama on a Mac said sunglasses: true. Diagnosed on the pod: the prompt’s worked example was being copied. Under prompt v4 (keys and types only), on the same frame through the same path: vision-rejected (“face covered by sunglasses”).

Two facts about Krea 2 the pass established. ② Restage keeps the persona’s identity for every person in a frame — a “second adult much closer to the camera” renders the persona’s face twice — so a stranger cannot be planted by prompt; the kit composites a ① Create of a second persona instead. And the sheet’s scene list holds a profile prompt loosely: the 24 + 4 renders measure profile L 0, ¾ L 7, frontal 17, ¾ R 3, profile R 1, all level, no full-body — the audit’s verdict on a fresh sheet is NOT READY with three blocking gaps, and its SHOOT NEXT is the Image Creator’s to-do list (HANDOFF §2).

What this package proves, and what it hands on

Proven here, on the pod, through the real App Mode form. A zip of photographs goes in; the audit measures every frame, culls what cannot train, ranks the rest, captions them, picks holdouts that span the angles, and writes input/<name>-audited.zip in the Krea 2 Character Creator’s exact layout at the exact size its loader uses, with the report rendered in the App view. The eight planted exercisers were judged as intended, eight of eight.

Handed on, and deliberately NOT proven here. Whether an audited set trains a better subject LoRA is a question about the TRAINER, answered by the Character Creator’s own likeness test on its own workflow. This package’s job ends at the zip. The evidence it hands over is two zips already sitting in that pod’s input/:

zip what it is
tstchr9-sheet.zip the raw 📇 character sheet, 24 train + 4 holdout, unaudited
tstchr9-audited.zip the same photographs after this audit: 23 train + 5 holdout, ranked, captioned, sized

One 600-step run on the raw sheet measured Likeness 0.57, “learned” in 23.5 minutes. The audited run belongs beside it, and the Character Creator’s session owns that measurement: it has the version whose saver frees ComfyUI’s models after training, without which two runs in one session exhaust the card (measured here: 88 GiB allocated of 95 GiB, on their V1.0.1).