Nodes/ComfyUI-BCNodes/Caption Audit
ComfyUI Node

Caption Audit

Catch the word that's in every caption before you train

By 0xBeycan·Created 23 days ago·Updated 3 days ago· 2
Caption Audit
    • report_image
    • report_text
    • report_json
    • critical
    • warning
    ◄directory►
    ◄trigger►
    ◄class_words►
    ◄fuse►
    ◄critical_threshold0.85►
    ◄warn_threshold0.60►
    ◄info_threshold0.35►
    ◄ngram_max3►
    ◄no_stopwordsfalse►
    ◄recursivefalse►
    ◄table_rows12►
    ◄images_dir►

    Here's the failure this node exists to prevent. You caption 30 images of your character, and every single caption says woman. You train. Now woman isn't an attribute any more - it's welded to your trigger, and you can't prompt her as anything else. The model never learned the difference between the trigger word and the thing that was in every picture.

    What it is

    A dataset audit for a LoRA caption set, rendered as a report card on the node. Point directory at a folder of images plus same-named .txt sidecars; the audit runs in-process and shows you the caption distribution before you spend an hour training.

    The measure is document frequency - in what fraction of captions a term appears at least once, across words, phrases and whole comma segments. A term near 100% can't be prompted in or out, so it becomes part of the identity. That's a dataset problem, not a training-settings problem, and nothing downstream of the dataset fixes it.

    The inputs that matter

    directory (the caption set), trigger (leave empty and it's inferred from the captions - the card marks that [INFERRED], and an inferred guess is worth checking), class_words (comma-separated, e.g. woman, car - shown as EXPECTED and never flagged, but coverage is measured per word, so one you declared and never actually wrote is a WARNING rather than a silent pass), and fuse - attributes you want welded to the trigger, e.g. red scarf. Those show as INTENDED and are excluded from the critical count.

    Then the cut-offs: critical_threshold 0.85 (fused with the trigger), warn_threshold 0.60 (strong bias, will bleed into unrelated prompts), info_threshold 0.35 (reported, not flagged). ngram_max sets the longest phrase analysed - phrases never cross a comma. no_stopwords includes function words, recursive descends into subfolders, and table_rows (default 12) changes the card's size, which is why it's the only input allowed to.

    Optional images_dir is for when captions and images are stored apart. With no images anywhere, the audit runs in caption-only mode.

    Outputs: report_image (the card), report_text (the full terminal report), report_json (identical to caption-audit --format json), plus critical and warning counts.

    What to wire

    critical is the reason this node can be in a pipeline rather than beside one: put it into a compare node ahead of the training branch and the workflow stops when the caption set isn't clean. And if the audit can't run at all - missing folder, no .txt files, thresholds out of order - the node renders an error card and returns critical = 1 rather than raising. A set that couldn't be checked has not been cleared, which is the correct default.

    The honest limitation, stated on the card itself: every flagged term has two possible causes with opposite fixes. Either it's caption hygiene (stop writing the word where it isn't the point) or it's dataset composition (you need contrast images that don't have the attribute). Telling those apart means looking at the images, which neither the node nor the package does.

    Installing it

    cd ComfyUI/custom_nodes
    git clone https://github.com/0xBeycan/ComfyUI-BCNodes
    pip install -r ComfyUI-BCNodes/requirements.txt
    

    This node needs the caption-audit package from requirements.txt. Without it the pack still loads; the Caption Audit node just isn't there. Same with Manager: search ComfyUI-BCNodes and take the requirements with it. Restart and reload the tab.

    The path restriction, and how to get around it properly

    directory and images_dir are widget values, and widget values come out of the workflow file - so a downloaded graph could otherwise point this node at any folder on your machine and read the caption text back out via report_text. Both paths are realpath-resolved and must land inside the ComfyUI tree (the root, input/, output/, user/).

    Real datasets live elsewhere, so the way in is an environment variable read from the shell ComfyUI starts in, never from the graph:

    BC_CAPTION_ROOTS=/path/to/datasets python main.py
    

    :-separated on Linux and macOS, ;-separated on Windows. A path outside those roots is refused on the error card with the roots listed.

    Where people get burned

    Small sets make lumpy percentages. On a 20-image set, document frequencies can only land on multiples of 5%, so a threshold of 0.85 is really "18 of 20". Nothing wrong with the tool - just size your expectations, and prefer the counts on the card over the percentage.

    An inferred trigger is a guess. Check it against a caption you actually wrote before you trust the critical list.

    It reads .txt only, never the images. So a caption that describes a detail the picture doesn't show is invisible to it - which is exactly the kind of error that shows up later as a LoRA that adds the wrong thing.

    CategoryBCNodes/analysis

    Inputs (12)

    NameTypeDefaultDescription
    directorySTRINGFolder holding the caption set: images plus .txt sidecars sharing each image's basename. Empty = ComfyUI's working directory.
    triggerSTRINGThe trigger word this LoRA is supposed to own. Leave empty to have it inferred from the captions — the card marks that [INFERRED], and an inferred guess is worth checking.
    class_wordsSTRINGComma-separated class words, e.g. 'woman, car'. Shown as EXPECTED and never flagged: they are supposed to be everywhere. Coverage is measured and shown per word — one you declare but never wrote into the captions is a WARNING, not a silent pass.
    fuseSTRINGComma-separated attributes you WANT welded to the trigger, e.g. 'red scarf'. Covers the phrase and its fragments, shown as INTENDED, excluded from the critical count. Keeps its row even at 0%, so a --fuse that no longer matches your captions is visible rather than inert.
    critical_thresholdFLOAT0.850.05–1Document frequency at or above which a term counts as fused with the trigger.
    warn_thresholdFLOAT0.600.05–1Strong bias: will bleed into unrelated prompts.
    info_thresholdFLOAT0.350.01–1Below this a term is not reported at all.
    ngram_maxINT31–5Longest phrase analysed. Phrases never cross a comma, so tag lists produce no phantom n-grams.
    no_stopwordsBOOLEANfalseOff (default) hides function words like 'a' / 'with' from the flag list. On shows everything.
    recursiveBOOLEANfalseDescend into subdirectories.
    table_rowsINT121–60Rows in the card's term table. This is the only input that changes the card's size — the canvas is otherwise fixed, so the node does not resize between runs. Unused rows show an em dash. Terms you declared always keep a row here.
    images_diroptSTRINGWhere the images live, when captions are kept in a separate folder. Empty = alongside the captions; with no images anywhere the audit runs in caption-only mode.

    Outputs (5)

    NameTypeDescription
    report_imageIMAGE—
    report_textSTRING—
    report_jsonSTRING—
    criticalINT—
    warningINT—