mirror of
https://github.com/vladmandic/automatic
synced 2026-09-18 16:54:33 +02:00
docs: document per-tag granularity, chain scalability, and stress test results
Adds everything validated in the latest round of testing: - multiple detections of the same class share one identical template - a full tag's text runs to the next newline, never implicitly split - typo detection is per-tag (not per-model or all-or-nothing), verified within a single multi-class model's own detections too - no code-level cap on chain length; verified with 7 chained models - 800-generation stress test: ~0.24ms/generation, ~0 net memory growth, fully deterministic, zero log output when everything resolves cleanly - no stale cross-generation state; removing a model from the chain produces an accurate orphaned-tag warning, not a false positive
This commit is contained in:
@@ -39,6 +39,8 @@ plain line with no tag → fallback pool
|
||||
- Class names are matched case-insensitively against the label YOLO reports (`model.names`), and comma-separated lists route multiple classes to the same text.
|
||||
- Lines with no tag are pooled as **positional fallback** — applied, in order, to any detection whose class had no matching tag, cycling if there are more untagged detections than fallback lines. A prompt with zero `[CLASS=...]` tags behaves exactly like it did before this patch.
|
||||
- Blank spacer lines are ignored when building the fallback pool (a blank line between a tagged and an untagged line doesn't consume a fallback slot).
|
||||
- If a class matches more than one detection (e.g. two `nipple` boxes from the same pass), every one of them gets the exact same tagged text — there's no per-instance variation, `[CLASS=nipple]` is one template shared by all detections of that class.
|
||||
- Everything after the closing `]` up to the next newline belongs to that tag, however long — a tag's text isn't limited to a short phrase, and a fallback line is only ever created by pressing Enter, never implicitly.
|
||||
|
||||
### Don't rely on fallback order for multiple classes
|
||||
|
||||
@@ -75,6 +77,8 @@ WARNING detailer Detailer prompt: class tags did not match any detection across
|
||||
|
||||
This had to be aggregate rather than per-model: chaining two detailer models with disjoint classes (say, a face-only model and a separate NSFW segmentation model reporting `nipples`/`pussy`/`anus`/etc.) is a completely normal setup, and a naive per-model check would flag `[CLASS=pussy]` as unmatched on every pass through the face model, and `[CLASS=face]` as unmatched on every pass through the segmentation model — pure noise despite both tags being perfectly correct. The aggregate version only complains when a tag never matches *any* model in the chain, which is the actual signature of a typo.
|
||||
|
||||
The check is **per-tag, not per-model or all-or-nothing.** Writing 6 correct tags and 1 misspelled one — whether across several models or all reported by a single multi-class model — flags only the one that's actually wrong; the other 6 resolve normally with zero noise. Verified with a single model reporting 6 real classes plus 1 typo in the same prompt: 6 clean resolutions, exactly 1 warning naming only the broken tag.
|
||||
|
||||
**It cannot tell a typo apart from a legitimate miss.** A correctly-spelled `[CLASS=face]` on an image where the face model simply found nothing this run (occluded, low confidence, out of frame) produces the exact same warning text as a real misspelling — the check only knows "declared tag X never matched a detection this generation," not *why*. To tell them apart, cross-reference the tag against the model's real vocabulary in the one-time `Load: type=Detailer name='...' ... classes=[...]` line printed when that model first loads; if your tag is in that list verbatim, it's not a typo, the class just wasn't found this time. That said, the warning is still useful either way it fires: it's a reliable signal that **"the `[CLASS=face]` prompt was not applied to anything this generation,"** regardless of the underlying cause — worth knowing on its own, independent of diagnosing why.
|
||||
|
||||
### Visual debugging: "Include detections"
|
||||
@@ -137,6 +141,22 @@ def assign_prompts(text: str, items: list) -> list[str]:
|
||||
|
||||
Tested with SDXL inpainting through a two-model detailer chain: a single-class face model (`face-yolo8n`, class `face`) followed by a multi-class NSFW segmentation model (`ntd11_anime_nsfw_segm_v5`, classes `nipples`/`pussy`/`anus`/`penis`/`cross-section`/`x-ray`/`testicles`). Debug log confirmed each detection received its own class-specific text (`label='face' ... prompt='...'`, `label='pussy' ... prompt='...'`) with zero false-positive typo warnings from the cross-model tag targeting, and one correctly-caught real typo (`[CLASS=gace]` against an actual detected `face`) before the fix, silenced immediately after correcting the tag.
|
||||
|
||||
Also confirmed: a typo caught within a *single* multi-class model's own detections (not just across separate models) still warns correctly — no false positive silence just because the correct and misspelled tags share one model.
|
||||
|
||||
## Scales to any chain length
|
||||
|
||||
No code path caps how many models can be chained (`detailer_models`/`detailer_args` is just parsed as a list, `restore()` loops over however many entries it gets) — nothing in this patch adds a limit either. Verified with a simulated 7-model chain, each contributing its own real class plus one deliberate typo mixed in: all 7 correct classes resolved cleanly, and exactly 1 warning fired, naming only the typo — no extra noise from chain length.
|
||||
|
||||
## Performance & robustness
|
||||
|
||||
Ran 800 simulated back-to-back generations (parse → resolve → aggregate-warning check, 3 chained model passes each) against the real `helper.py` code:
|
||||
|
||||
- **Speed:** ~0.24 ms per generation for the entire class-prompt resolution + warning pass — negligible next to actual YOLO inference / diffusion inpainting, which run in seconds.
|
||||
- **Memory:** net growth after 800 iterations was ~1 KB, and that KB was traced to the memory profiler's own bookkeeping, not this code — every value used (`prompt_classes`, `matched_prompt_classes`, etc.) is a local variable inside `restore()`, created fresh and discarded every call. Nothing persists or accumulates between generations.
|
||||
- **Determinism:** generation 1 and generation 800 produced byte-identical output for identical input.
|
||||
- **No stale state across config changes:** class names come from `item.label` on live detection results, generated fresh every run — never cached or reused from a prior generation. Swapping which models are in the chain, or removing one entirely, takes effect immediately on the next generation with no leftover behavior from before. If you remove a model and leave behind a `[CLASS=...]` tag that only that model could ever satisfy, you'll correctly get an "unmatched" warning for it (an orphaned tag, not a false positive) — not silence, and not a stale match.
|
||||
- **Log noise:** when every tag resolves correctly, this patch emits *zero* additional log output, generation after generation — nothing to scroll back through. The warning only appears the moment something is actually wrong, and only that one line.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- This is a parsing convention layered on top of the existing flat `detailer_prompt`/`detailer_negative` strings — there's still no per-model or per-class field in the request schema. Anyone driving the API directly (not through the WebUI textbox) gets the same syntax for free, since it's resolved server-side regardless of how the string arrived.
|
||||
|
||||
Reference in New Issue
Block a user