qwen21-nsfw-pose-skill: When an Agent Has to Click

A 136-pose 3D mannequin library, a Qwen-Image 2.1 workflow, and the unglamorous plumbing that makes agent-driven image generation work: a browser it must drive, a GPU lock it must respect, and a doctor that refuses to crash.

9 min read · Python • View on GitHub • More from Arekejoker

A wide black-ink engraving of a browser window drawn as a small proscenium stage. Inside the stage stands a jointed wooden artist's mannequin with one arm raised mid-pose. A mechanical arm reaches in from the right edge, its single finger resting on a mouse, and a thin cord runs from the mouse, over the browser chrome, and into the mannequin's shoulder. Faint server racks are suggested in crosshatch behind the stage. The illustration captures the article's thesis: the pose editor cannot run headless, so an agent must drive the web app the way a person would.
The mannequin is the pose library, the stage is the frontend, and the cord from the mouse is the only channel that works. Nothing here says image generator, which is the point.
Key Takeaways

A Node That Only Exists in a Browser

The most unusual line in qwen21-nsfw-pose-skill is not about image generation. It is about a browser tab.

VNCCS_PoseStudio, the node that applies a stored 3D mannequin pose to a render, needs the frontend's live capture at execution time. Its output depends on a canvas that the page's own JavaScript draws, so the ordinary ComfyUI HTTP route is closed. You cannot POST a job and wait for a file.

So the script does what a person would do. It connects to Chromium over the Chrome DevTools Protocol, loads the workflow graph into window.app.loadGraphData, sets the widget values, queues the job from the UI, and then reads the result off disk. An agent skill that needs a human-shaped GUI to function is a contradiction worth sitting with.

The file's own docstring states the constraint plainly: "VNCCS_PoseStudio needs the frontend's live capture at execution time, so we load the graph in the shared browser (our own tab), set the character image and the pose preset, queue from the UI, then collect the outputs from disk." That single sentence defines the repo. Everything else is scaffolding around it.

Anyone building agent tooling on top of GUI-first software will meet this wall eventually. The application was designed for a human at a screen, and its most valuable state lives in the DOM, not in an API. The workaround is brittle by nature. A re-exported workflow, a renamed widget, or a changed node id can break the whole chain, and the breakage may not announce itself.

What This Repo Actually Is

Strip away the framing and the repo is a shell. It does not ship a model. It ships a configuration: a Qwen-Image 2.1 int8 diffusion model at roughly 6.8 GB, a Qwen3-VL 8B text encoder, a bf16 VAE, and a set of NSFW and pose LoRAs. ComfyUI does the inference. The skill's job is to make that stack reproducible and operable by an agent.

It offers three routes. Text-to-image with a LoRA. An edit pass over an existing character image. And the VNCCS pose route, which pulls a named pose from a 136-pose library assembled from four public Hugging Face collections plus 55 curated entries. The poses are bone rotations rendered through a MakeHuman mesh and three.js, so a pose is data, not a prompt.

It ships as an agent skill, a SKILL.md instruction file that Claude Code, Codex, Cursor, and OpenClaw can load, and it also runs as a plain CLI. The adult content is the point of the project, not a side effect. It is in the name, and it is stated without winking. That framing also decides who the tool is for: people already running a local ComfyUI instance, not casual users who want an app.

One number deserves care. The README states a 12 GB NVIDIA VRAM minimum and a 24 GB recommendation. Those are project-published claims, not independent benchmarks, and the research behind this article found no third-party measurement to confirm them.

Three Routes, One Recipe

Each route is a different entry point into the same ComfyUI graph, and that graph is never edited on disk. The harness deep-copies the workflow JSON, injects parameters into the copy, and submits the copy. The production file stays untouched, which means a bad run cannot corrupt the working setup.

The interesting injection is the LoRA. inject_lora_model_only finds the UNETLoader node by class type, inserts a LoraLoaderModelOnly node at a fixed id, and rewires every consumer of the model output to point at the LoRA instead.

def inject_lora_model_only(wf, lora_name, strength):
    unet_id = next(nid for nid, node in wf.items()
                   if node.get("class_type") == "UNETLoader")

    wf["99"] = {
        "class_type": "LoraLoaderModelOnly",
        "inputs": {
            "lora_name": lora_name,
            "strength_model": strength,
            "model": [unet_id, 0],
        },
    }

    # Rewire every consumer of the UNET model output to the LoRA.
    # The text-encoder path is deliberately left untouched.
    for node in wf.values():
        for name, val in node.get("inputs", {}).items():
            if val == [unet_id, 0]:
                node["inputs"][name] = ["99", 0]

That last comment is the whole design decision. DiT-only LoRAs are trained on the transformer, not the text encoder, so patching the text-encoder path would quietly change how the model reads the prompt. The code patches one path and leaves the other alone on purpose.

Around the graph sits a prompt recipe: seven elements, 300 to 1500 characters. The format is the part the community picked up first, before anyone noticed the browser plumbing underneath.

qwen21-nsfw-pose-skill is up on github now, means you can do pose-controlled nsfw image gen in agent workflows with Qwen-Image 2.1 and comfyui. prompts need 7 elements, length is 300–1500 chars. saw 1,181 stars already. https://t.co/pZAb5RGQu8

Kumar Deepanshu, @kumard_3 · @kumard_3 on X

The Prompt You Set Three Times

Here is where the repo gets strange in a good way. Setting the capture prompt is not setting a prompt.

The frontend rebuilds the capture prompt from the scene document's export block. That template reads Draw character from image2, then a lighting field, then a user prompt. But library pose files ship with empty prompt fields and no export block at all. The frontend reads whichever serialized source it finds first, and two of the three are blank by default.

So the script writes the prompt into three hydration paths at once: scene.export.prompt_template, pose.prompt, and pose.pose_prompts. Miss one and the renderer quietly uses the wrong text. The author documented this in a code comment rather than fixing it, because the frontend's behavior is outside the skill's control.

This is the bug class anyone driving a GUI-first app through automation will eventually meet. The state that matters is spread across several serialized fields, the application reads them in an order you do not control, and nothing warns you when a path is empty.

Toggle off the scene export block while the pose fields are empty and the prompt collapses to a bare instruction with no lighting and no user prompt. That is the silent failure the script exists to prevent.

The Plumbing Nobody Writes About

Two files carry the lessons that transfer beyond this repo, and neither is about image generation.

A close-up black-ink engraving of a brass turnstile set into a walled corridor. To the left, a queue of small identical figures waits in a shared lane. To the right, a single large crate on a hand truck passes through a gate marked with an exclusive arrow. A tiny clock face is bolted to the turnstile housing, its hand near the end of a long arc, and the spokes are caught mid-rotation. The image makes the media lock legible: shared image jobs pass together, an exclusive video render takes the whole gate, and the clock is the wait deadline.
media_lock.py arbitrates the GPU with a POSIX flock. Image jobs take a shared lock and run together; a video render takes an exclusive lock and everyone waits behind it.

media_lock.py arbitrates the GPU across processes. Video renders take an exclusive lock. Image jobs take a shared lock, so several can run at once but all of them wait behind a video render. The wait uses a monotonic deadline and raises MediaLockError if it expires.

flags = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
deadline = time.monotonic() + wait_timeout

while True:
    try:
        fcntl.flock(f, flags | fcntl.LOCK_NB)  # non-blocking try
        break
    except BlockingIOError:
        if time.monotonic() >= deadline:
            raise MediaLockError("timed out waiting for media lock")
        time.sleep(2)  # poll until deadline

It is advisory and single-host. It cannot stop a process that ignores the lock, and it depends on a shared filesystem. The docstring also claims video rendering stops the local LLM for the duration, but that behavior is not in this file. It lives in a model-switching script outside the repository, which is an undocumented dependency hiding in a comment.

doctor.py is the opposite of a fragile script. It is read-only by contract: it never writes files, changes ComfyUI state, installs anything, launches a browser, or takes the media lock. Each check is independently guarded, so one failure degrades to a warning instead of taking down the run. Its exit code is non-zero only for a FAIL, never for a WARN or a SKIP. And it asks ComfyUI what it actually has installed rather than assuming, by reading the combo-box options that the /object_info endpoint returns.

The third lesson is a documented silent failure. In make_custom_pose.py, loadFromLibrary has to receive a library list entry, not pose file content. Pass the wrong shape and the fetch fails, the editor keeps its previous pose, and nothing raises. A script would happily save the wrong pose and report success. The author found this, wrote it into the docstring, and built the lookup around the correct entry shape.

What It Isn't

The honest way to place this repo is to be specific about what it changes and what it does not. It is an integration and reproducibility layer. It is not a better pose model, and it does not fix the fine details that image generation still struggles with.

What controls the posePrecisionSetup costWhat it does not solveWhere it lives
ControlNet / OpenPose skeleton workflowsHard skeletal guidance from a keypoint imageHigh for limb placementInstall ControlNet plus a keypoint sourceHands, faces, clothing detailComfyUI custom nodes
Natural-language pose promptingText description interpreted by the modelLow, the model decidesNothing beyond a working modelReproducibility and exact anglesAny prompt interface
Character LoRAsLearned identity and style tendenciesNot a pose control at allTrain or download a LoRAPose placementModel weights
This skill's VNCCS routeNamed 3D mannequin pose from a 136-pose libraryHigh for pose, curated and repeatableComfyUI plus VNCCS nodes and the pose libraryHands, expressions, clothing detailLocal ComfyUI instance

The differentiator is the packaging, not the pose model. Named poses, prompt recipes, workflow JSON, and agent instructions in one place. The trade-off is narrow scope: tied to one model and one workflow, with adult-oriented materials included, and no guarantee of precise hands, expressions, or clothing.

它本质上是一个工程化补丁,不是模型升级。

Fengdao AI Tools reviewer, Reviewer, Fengdao AI Tools · Fengdao AI Tools review

The Rule About Weakened Outputs

The skill's instructions carry a governance rule that is easy to miss: a weakened output must be disclosed rather than silently delivered. If a render comes out degraded, the agent is told to say so. That is a small rule with a large implication, because most generation stacks have no opinion about honesty. The output is treated as the user's problem.

The repo also raises questions it does not answer. The 136-pose library is assembled from four upstream Hugging Face collections, so attribution and license terms matter. Likeness rights matter when a character image is edited. Distribution of adult material is governed by local law and by whatever platform hosts the output. None of that is settled by a SKILL.md.

It is worth stating the timeline plainly. Research surfaced no verified author interview, no conference talk, and no established controversy. The clearest third-party commentary is a review that calls the project an engineering patch rather than a model upgrade. The star growth is real but dated, and visibility is not adoption. Claims of impact should stay modest.

Qwen-Image 2.1 NSFW/姿势出图 skill:作者级提示词配方 + NSFW LoRA + 136 姿势 VNCCS 人偶控制;适用 Claude Code / Codex / Cursor / OpenClaw

Arekejoker/qwen21-nsfw-pose-skill project, Project description · Arekejoker/qwen21-nsfw-pose-skill

The lesson that outlasts the adult framing is the plumbing. When the software you want an agent to operate was never built to be operated, the shell around it becomes the product. A browser it must drive. A lock it must respect. A doctor that refuses to crash. And a prompt that has to be set three times, because the frontend never agreed to read it once.