← ExpSoft Uncensored Captioner

Tag-forced captioning — pairing a frozen visual tagger with a free-form VLM for honest dataset descriptions

ExpSoft Uncensored Captioner is a personal-use Windows EasyInstaller around two open-source models published independently by their authors: the Qwen-VL family of vision-language models from Alibaba Cloud (Qwen Team) — distributed under Apache 2.0 / Qwen License — used here via the “abliterated” community variants prepared by huihui-ai and prithivMLmods, and the WD SwinV2 Tagger v3 from SmilingWolf under Apache 2.0. The legal frame is personal experimentation around open-source models that each user accepts the license of when downloading (see the Legal & Compliance section of the product page). ExpSoft is an independent installer, not affiliated with any of the upstream authors. The signature engineering choice — and the reason a dataset of 2 000 images can be captioned overnight on a 12 GB consumer GPU — is the “tag-forced” workflow: WD14 produces booru-style tags first, those tags are injected into the prompt that the VLM sees, and the VLM is asked to write a flowing prose description that must naturally incorporate every listed tag. The result is prose that doesn’t drift, doesn’t omit details, and stays uniform across a large dataset. The supporting engineering — 4-bit NF4 quantisation, a max-pixel cap on the visual processor, aggressive inter-image VRAM cleanup, batch resumability — is what makes the whole loop fit a personal workstation.

· 13 min read · By Nicolas Riquier

The captioning bottleneck in personal dataset prep

If you’re training a LoRA, fine-tuning a diffusion model, or hand-rolling a dataset for an embedding, the bottleneck very quickly stops being model size and starts being captions. A modern diffusion model trains best on flowing natural-language descriptions of what is in each image — not a flat list of booru-style tags, not a one-sentence summary. The model wants prose that names what the subject is wearing, what pose they’re in, what the camera angle is, what the lighting is doing, what the environment looks like.

Producing those captions at scale is hard for two practical reasons:

The Uncensored Captioner is built around a workflow that addresses both problems at once. The framing is professional and the legal frame is explicit — users are captioning content for which they hold the appropriate rights, on their own machine, with no upload, no cloud, no telemetry.

What the “abliterated” variants are, and why they’re used here

The Qwen-VL family is a series of vision-language models published by Alibaba Cloud’s Qwen Team. The base models are excellent at visual description in general — they’re the upstream work that makes everything in this article possible. They are, however, instruction-tuned to refuse certain content categories.

The open-source community has published “abliterated” variants of these models — community-prepared checkpoints where the refusal direction in the model’s representation space has been surgically projected out, leaving the visual-description capability intact and removing the refusal behavior. The relevant authors and their permissive license terms:

The installer offers five caption-model choices in the Run tab’s model loader (3 from huihui-ai, 1 from prithivMLmods, plus the prithivMLmods 7B Caption-it). All five are downloaded only at the user’s request, through the user’s own HuggingFace credentials — none of the weights are bundled in the installer ZIP. The user accepts each upstream license at download time. ExpSoft is an independent installer and is not affiliated with Alibaba Cloud, huihui-ai, prithivMLmods, or SmilingWolf.

The tag-forced workflow — the signature feature

The most interesting engineering inside the app is not the VLM inference (the upstream models do the heavy lifting). It’s the pipeline that ties two independent models together for a result neither produces alone.

Step 1 — WD14 generates booru-style tags

SmilingWolf’s WD SwinV2 Tagger v3 is a SwinV2-based image classifier trained on a large corpus of booru-style tagged images. Given an image, it emits a list of tags with confidence scores: 1girl, blue eyes, smile, simple background, looking at viewer, ... The tags fall into three categories — general (visual content), character (named characters when recognised), and rating (the overall safety level: general / sensitive / questionable / explicit).

Inference is fast — a single SwinV2 forward on a 448 px image, running through ONNX Runtime with the CUDA execution provider when available. On a consumer GPU it’s a few tens of milliseconds per image. The tagger’s output is filtered to keep only tags above a configurable confidence threshold (default 0.85), then optionally further filtered by category according to the user’s settings (include character tags, include rating tag).

Step 2 — tags are injected into the VLM prompt

The resulting tag list is injected into a prompt template that the VLM will receive along with the image. The default template:

Describe this image in detail for AI training. You MUST naturally
incorporate ALL of the following elements in your description:
{tags}. Write detailed, flowing sentences that include every listed
element. Do not simply list the tags.

The {tags} placeholder is replaced with the comma-separated list from WD14. The wording is deliberate — it instructs the VLM that the tags are the ground truth that must be covered, that the output must be prose rather than a list, and that the VLM has the latitude to add any context it sees that the tagger missed (the tagger is good at the visual-content axis but blind to ambience, mood, and global composition).

Step 3 — the VLM writes the description

The chosen Qwen-VL variant receives the image and the prompt-with-tags and produces a flowing description. The output is constrained by the prompt to cover every listed tag, so the model is much less likely to skip the visual elements the tagger identified. The output is freer than a flat tag list — the model fills in everything the tagger doesn’t know (lighting direction, atmospheric quality, subject expression, the implied context of the shot).

The result is a caption that combines the strengths of both models:

Step 4 — pre-tags, post-tags, multi-replace

Three post-processing affordances are applied to the VLM’s output before it’s written as the caption:

The combination is dataset-wide consistency by construction: every caption starts with the same trigger, every caption ends with the same quality tag, and any vocabulary normalisation happens once at write-time rather than as a post-process pass later.

The supporting engineering

The workflow above is the “what.” The reason it runs on a 12 GB consumer GPU is the supporting engineering.

4-bit NF4 quantisation by default

Qwen3-VL-8B in fp16 is around 16 GB of VRAM — comfortably above the budget of the typical consumer card (8-12 GB still represents most of the installed base). In 4-bit NF4 via bitsandbytes the model footprint drops to roughly 5-6 GB. The quality difference for captioning, in side-by-side testing on a representative dataset, is in the “a few percent” range — visible if you look hard at fluency on rare vocabulary, indistinguishable on coverage and accuracy. The default in the UI is 4-bit on; a user with 24 GB+ of VRAM can flip it off for a small quality bump.

max_pixels = 512 × 28 × 28 — bounding the visual-token count

Qwen-VL processes images by partitioning them into 28×28-pixel patches. A 4K image generates well over ten thousand visual tokens — generation slows dramatically and the KV cache balloons past what a 12 GB card can hold. The processor exposes a max_pixels parameter that limits the effective resolution of the partitioning.

The app sets max_pixels = 512 * 28 * 28 = 401408 (≈ 633 × 633 effective resolution), with an additional Pillow downscale to 1024 px on the longest side before the image even reaches the processor. The combined budget caps the visual token count at roughly 512 tokens — enough for the VLM to “see” the image well, small enough to keep the KV cache and generation time bounded.

The quality cost is negligible. A VLM looking at an image for description is doing global understanding, not OCR or fine-detail recognition — a 633 × 633 effective resolution captures everything that matters for the captioning task on this generation of models.

Aggressive inter-image VRAM cleanup

Without explicit cleanup between batch iterations, the VRAM consumption climbs from one image to the next — PyTorch’s caching allocator holds onto intermediate tensors, the KV cache from the previous generation lingers, and after thirty or forty images the OOM lands. The captioning loop deletes every intermediate tensor explicitly after writing the caption (inputs, generated_ids, image_inputs, video_inputs, text, the message dict), then calls gc.collect() and torch.cuda.empty_cache().

The verbose memory snapshots are logged to logs/mem_debug.log before and after each step, which makes the very rare OOM-on-image-N case actionable — the user can see exactly which tensor wasn’t released and bring it up against an upstream release.

Skip-existing for batch resumability

A 2 000-image dataset takes several hours to caption even on fast hardware. The PC will go to sleep, the user will reboot for an update, something will crash, the run will be interrupted at image 1 500. The Skip Existing checkbox (default on) makes the batch idempotent: before captioning an image, the loop checks for a .txt file at the target name and skips that image if one is present. Re-launching the batch finishes the remaining 500 images without redoing the 1 500 that completed before the interruption.

WebSocket + REST hybrid backend

The Python backend is a FastAPI server running under uvicorn on a local port. Two transports between the C# UI and the backend:

The batch status is polled rather than pushed — GET /batch/{job_id}/status every two seconds. Pushing the status over WebSocket would have required a second persistent connection per session, with reconnect logic; polling is simpler, and a 2-second cadence is well within UI responsiveness for a batch that processes one image every 5-15 seconds. The trade is the right one for this scale.

Portability — same Pinokio-style isolation as the rest of the catalogue

As elsewhere in the ExpSoft catalogue, the entire install lives under the install folder:

Nothing in %USERPROFILE%, %APPDATA%, or the registry. PathService.AutoPatchVenvPaths() runs at every Run-tab launch and rewrites any hardcoded paths in the venv if the install location has changed since last run. The user can zip the entire install folder, drop it on a different machine with a different drive letter, double-click, and the captioning workflow runs.

The three build variants and the GPU mismatch warning

The PyTorch wheel sets for cu124, cu128, and cpu are mutually exclusive. The installer ships in three flavours:

The C# project file holds these as three Release configurations with DefineConstants; the Python-facing constants (TorchVersion, TorchCudaIndex, TorchIndexUrl) are #if-gated. At Setup step 3, the GPU detection step compares nvidia-smi’s reported card series against the build variant and shows a friendly warning if there’s a mismatch — “you have a 50xx-series card but downloaded the 40xx package; please use the 50xx package for full performance.”

Credits and license

The C# / WPF EasyInstaller shell is original ExpSoft work. None of the model weights are bundled in the installer — every caption model and the tag model are downloaded with the user’s HuggingFace credentials at Setup time, which records the user’s acceptance of each upstream license. ExpSoft is an independent installer and is not affiliated with Alibaba Cloud, huihui-ai, prithivMLmods, SmilingWolf, or any other upstream author.

Take-aways

Frequently Asked Questions

Quick answers to what people ask AIs about this article specifically.

What is the “tag-forced” workflow, and why does it produce better captions?

It’s a two-model pipeline: first, SmilingWolf’s WD SwinV2 Tagger v3 (a visual classifier trained on booru-style tags) generates a high-recall list of visual elements in the image; then those tags are injected into the prompt that the Qwen-VL variant receives, with an instruction that the VLM must naturally incorporate every listed element in its description. The result combines the recall of the tagger (which rarely misses a clearly-visible element) with the prose fluency of the VLM (which adds context, atmosphere, and global composition the tagger can’t see). The captions are uniform in coverage across a large dataset, which is what diffusion-model training actually needs.

Why does 4-bit NF4 quantisation work for captioning when it’s often considered too aggressive elsewhere?

Captioning is a generation task where the output is natural-language prose describing what’s visible in the image. The model needs to be confident enough in its visual understanding and language generation to produce fluent text covering the right elements. NF4 quantisation produces a small (few percent) degradation in raw output fluency — visible if you compare side-by-side on rare vocabulary, indistinguishable in caption coverage and accuracy. For dataset-prep captioning where the output is used to condition a diffusion model’s training, the small quality cost is a strong trade for the 3× VRAM saving that unlocks consumer-GPU usage.

Why are the model weights downloaded at first-run rather than bundled in the installer?

Two reasons. First, the licenses of the upstream models (Apache 2.0 / Qwen License from Alibaba Cloud, Apache 2.0 from huihui-ai, prithivMLmods, and SmilingWolf) are accepted individually by the user at download time when they go through HuggingFace with their own credentials. Bundling the weights in a third-party installer would short-circuit that per-user acceptance. Second, the combined size of the caption models is around 16 GB each — an installer with bundled weights would be a huge download, much of which the user might not use. First-run download lets the user pick exactly which caption model(s) they want.

What happens to memory between images in a long batch?

Without explicit cleanup, PyTorch’s caching allocator and the KV cache from each generation accumulate, and the VRAM climbs from one image to the next until OOM lands at image 30-40 on an 8 GB card. The captioning loop deletes every intermediate tensor explicitly after the caption is written (inputs, generated_ids, image_inputs, text, the message dict), then calls gc.collect() and torch.cuda.empty_cache(). The VRAM footprint stays flat across thousands of images. Memory snapshots before and after each step are logged to logs/mem_debug.log for diagnostics if the very rare OOM still occurs.

Can the captioning run be resumed after an interruption?

Yes — that’s what the “Skip Existing” checkbox (default on) is for. Before captioning each image, the loop checks whether a .txt file already exists at the target name and skips the image if so. After an interruption (sleep, reboot, crash), re-launching the batch resumes naturally: the first 1 500 images that completed are skipped instantly, the remaining 500 are processed. The pattern makes long batches robust to the practical interruptions that always happen on a personal machine.

Want to use ExpSoft Uncensored Captioner?

Get it on Patreon →