A single NSFW score can't moderate a multi-feature app. Here's what we use instead
We replaced a binary NSFW classifier with Cloudflare's Clef decision model on Workers AI. Clef answers five descriptive questions about each image (how sexual it is, whether a minor or a person appears, whether it is gor
We replaced a binary NSFW classifier with Cloudflare's Clef decision model on Workers AI. Clef answers five descriptive questions about each image (how sexual it is, whether a minor or a person appears, whether it is gory, and whether it fits the feature's scene), and code applies a separate policy per feature. The same swimwear photo can now be fine for editing and blocked for video, which one NSFW score could never express.
Why can't one NSFW score moderate every feature?
ImgPilot is an AI photo site with several features that take user photos: a general photo editor, a photo-to-video generator, and an outfit try-on tool. Every uploaded image is moderated before it reaches a generation model.
Until November 2026, moderation was one classifier on Replicate, falcons-ai/nsfw_image_detection (a fine-tuned ViT), plus an nsfwjs hint in the browser. The classifier returns one label, normal or nsfw. It has no idea which feature the image is for, and the same image does not have the same answer in every feature:
- A beach photo in swimwear is an ordinary upload in a photo editor. Turned into a video of a real person, the same photo is easier to misuse.
- A photo of a child is normal in a family photo to restore. In an outfit try-on tool, a child's photo should never be processed at all.
- A lingerie product shot is a normal clothing reference for try-on. Uploaded as the person to dress, it calls for a stricter look.
A binary classifier can only be tuned toward blocking more or blocking less. Whichever way the threshold moves, one feature gets the wrong answer, and adding rules on top does not help because the input never says which feature is asking.
There was a second, quieter problem. Over five months, 283 of 686 server-side upload checks (41%) timed out or failed, between 30% and 59% in any month. The upload path allowed images through when the check failed, so those images were never reviewed.
What is a decision model, and why Clef?
A decision model takes a state and a set of typed questions, and returns a probability for every allowed option of every question in one pass. Cloudflare's Clef models follow the request format of TypeSafe's Jev, run on Workers AI and accept images. For moderation, the important property is that the question can include the product scene, so the model reads the image and the context together.
| Clef (Workers AI) | Jev 1.14 (TypeSafe) | |
|---|---|---|
| Image input | PNG, JPEG, WebP, up to 4 images per call | PNG, JPEG behind an opt-in header the docs call not yet production-ready |
| Where it runs | Workers AI binding inside the Worker | Hosted API |
| Data retention | Zero data retention | Per DPA; ZDR for enterprise |
| Price (input, per M tokens) | $0.24 (Clef), $0.038 (Clef-flash) | $0.042 |
We use Clef (27B). Our Worker already runs on Cloudflare, so the call is a binding with no API key, and image support is generally available. At our volume, price did not decide anything: a check uses roughly 1,300 to 1,900 input tokens, about $0.0004 per image.
How do you make moderation depend on the scene?
The key decision was not to ask the model "does this image violate the rules?". Clef answers five questions that describe the image instead:
- sexual: none, suggestive (revealing but covered) or explicit (nudity, see-through clothing, sexual acts)
- minor: does a person appear to be under 18
- person: is a real person visible at all
- gore: graphic violence or injuries
- scene_fit: given the scene description, should this image be accepted
The decision then happens outside the model, in two layers. The first layer is a short list of hard rules that apply to every feature: explicit content, anything sexual involving a possible minor, and graphic gore are always blocked, and no feature can relax them. The second layer is a policy per feature, which we call a scene. Each scene says what it tolerates and carries a plain-language description that the model reads when it answers the scene_fit question:
| Scene | Allowed | Blocked |
|---|---|---|
| Photo editing | Everyday photos, swimwear, children in family photos | Only what the hard rules block |
| Photo to video | Portraits, couples, everyday swimwear | Photos whose main purpose is sexual appeal |
| Outfit try-on: person | Adults, including swimwear and lingerie | Children, and photos with no person in them |
| Outfit try-on: garment | Clothing shots, including underwear and kids' clothing | Images that are not clothing references |
A new feature picks an existing scene or adds one. Nothing about the model or the questions changes.
Splitting description from decision gives three practical benefits. The rules live in code, so they are reviewed and tested like code. Thresholds change without touching the prompt. And every block has a reason that points at one probability, which makes a false positive easy to explain.
The scene description still matters, but it is one input among five and cannot approve an image on its own. One example of why the split pays off: the outfit try-on person slot kept accepting clothing product shots, because "does this fit the scene" was a vague question. A dedicated person question fixed it cleanly: real photos of people scored 0.01 to 0.02 for "no person", and garments and objects scored 0.99.
How did we check that scene-aware decisions work?
We labelled 45 images by where they should be blocked, not by a single NSFW flag: everyday photos of people, objects, clothing product shots, ordinary photos of children, swimwear, sexualized but clothed photos, and explicit images. Each image has an expected result in every scene. The swimwear and lingerie test images were generated with an image model, whose own safety filter refused 6 of the 18 prompts.
| Scene | Correct | Missed | Wrongly blocked |
|---|---|---|---|
| general | 42 / 45 | 2 | 1 |
| clothes_person | 43 / 45 | 2 | 0 |
| clothes_garment | 42 / 45 | 2 | 1 |
| clothes_result | 41 / 45 | 2 | 2 |
| video_source | 42 / 45 | 1 | 2 |
Judged against the general scene, the old classifier caught 3 of 7 explicit test images and blocked 3 swimwear or lingerie photos that the general scene allows. Clef with the hard rules caught 5 of 7 and blocked 1 such photo. The two misses were clothed but sexualized photos, which the general scene allows by design and the video scene blocks.
One finding surprised us: Clef follows the definitions you give it very literally. See-through lingerie first came back as "suggestive", because our definition of explicit only mentioned exposed nudity. After the definition named see-through and barely-covering clothing explicitly, and the explicit threshold moved from 0.5 to 0.3, those images were blocked. Clear definitions matter more than model size.
The limits are real. 45 images is a small set, the gore category has no samples yet, and borderline cases such as a mirror selfie in a bikini are judgment calls that will keep moving with more data.
Before and after
| Replicate NSFW classifier | Clef with scene rules | |
|---|---|---|
| Basis for a decision | One normal / nsfw label, same in every feature | Five descriptive answers and per-scene rules in code |
| The same swimwear photo | Same result in every feature | Allowed for editing, judged by the scene rule for video |
| Photos of children | Cannot be handled separately | Allowed for restoration, blocked for outfit try-on |
| Explicit test images caught | 3 / 7 | 5 / 7 |
| Allowed photos wrongly blocked | 3 | 1 |
| Failed checks | 283 of 686 (41%), allowed through | 0 of 294 in the first two days |
| Check latency | Median about 3 s | Median 1.8 s, p95 5.6 s |
How do you handle content that is fine in one feature of your product and not in another?
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.