Filter AI-generated images before showing them
Last updated 2026-11-09
An image generator will produce what the prompt asks for, and a prompt will ask for anything. If your product shows generated images to users, the output needs a check between the model and the screen. This guide scores every generated image with the SFW API before it is returned: you get a nudity level from 0 (Everyday) to 3 (Explicit), an art flag that lets you judge stylised output on its own terms, and a blocked verdict against a policy you choose. Blocked outputs are regenerated or replaced; nothing is stored along the way.
Why a level instead of a yes/no
Generated images are often stylised, and the line between acceptable and not is a product decision, not a constant. One service allows swimwear and blocks nudity; another allows painted nudes and blocks photorealistic ones; a children's app blocks revealing outfits entirely. A single "unsafe" bit cannot express any of that. The level and the art flag can, and the policy object turns them into one boolean on the API side so the pipeline code stays short. The levels page defines the four levels; in short, pose, framing and cleavage never raise the level, so you can trust a 2 or 3 when you see one.
The pipeline hook
The example uses Diffusers in Python; the same shape works with any generator that gives you image bytes, including hosted APIs that return base64.
pip install sfwapiimport io
import os
from sfwapi import SfwApi, SfwApiError
api = SfwApi(api_key=os.environ["SFWAPI_API_KEY"])
# Bare skin allowed, nudity blocked, adult objects blocked, drawings blocked only when explicit.
POLICY = {"block_level": 2, "block_sex_objects": True, "art": "block_from_3"}
def to_webp(image) -> bytes:
buf = io.BytesIO()
image.save(buf, format="WEBP", quality=90)
return buf.getvalue()
def generate_checked(pipe, prompt: str, attempts: int = 2):
"""Return (image, result) for the first acceptable output, or (None, result) after `attempts` blocked tries."""
result = None
for _ in range(attempts):
image = pipe(prompt).images[0]
result = api.analyze_bytes(to_webp(image), policy=POLICY)
if not result["policy"]["blocked"]:
return image, result
return None, resultAnd the caller, for example inside a FastAPI route or a queue worker:
image, r = generate_checked(pipe, prompt)
if image is None:
return {"error": "The prompt produced an image we cannot show.", "level": r["nudity"]["level"]}
return {"image": encode(image), "level": r["nudity"]["level"], "art": r["flags"]["art"]["detected"]}If generation runs on a GPU box without the rest of your stack, call the API from there: the key lives in that process's environment and the bytes never travel through a second service.
Hosted generators and TypeScript
Hosted image APIs usually return base64. The TypeScript SDK accepts it directly, so a Node service that proxies a generator becomes:
import { SfwApi } from '@sfwapi/sdk';
const api = new SfwApi({ apiKey: process.env.SFWAPI_API_KEY! });
const POLICY = { block_level: 2, block_sex_objects: true, art: 'block_from_3' } as const;
export async function generateChecked(prompt: string, attempts = 2) {
for (let i = 0; i < attempts; i++) {
const b64 = await generator.create(prompt); // your provider's call, base64 PNG
const r = await api.analyzeBase64(b64, { policy: POLICY });
if (!r.policy?.blocked) return { image: b64, level: r.nudity.level, art: r.flags.art.detected };
}
return null;
}Base64 costs about a third more bandwidth than a multipart upload; for batch jobs convert to bytes and use analyzeFile instead. Either way the response is identical.
Choosing the policy
block_level sets the line for photorealistic output. art sets it for output the model recognises as drawn, painted or stylised 3D:
| Product | Policy |
|---|---|
| General-purpose generator for all ages | {"block_level": 1, "block_sex_objects": True, "art": "block_from_2"} |
| Creative tool, bare skin fine, nudity not | {"block_level": 2, "block_sex_objects": True, "art": "block_from_3"} |
| Art platform that allows painted nudes | {"block_level": 3, "block_sex_objects": True, "art": "allow"} |
art: "allow" skips the level check for frames flagged as art entirely, so use it only where illustrated nudity is in scope for the audience. same_as_photos (the default) judges drawings exactly like photos. The policies page explains how the art rule and the level interact frame by frame.
Reading the response
nudity.levelandnudity.scores: the headline and the three calibrated numbers behind it; above 50 counts, 35 to 65 is the grey zone.flags.art.detected: whether the image was judged as art, which also tells you the art rule was applied.flags.sex_object.detected: adult toys and objects, independent of nudity.policy.blocked,policy.reason(level,sex_objectorart_level),policy.matched_level.usage.creditsandusage.balance.
Two useful extras for a generator: show the level to the prompt author as feedback ("this came out at level 2, try a different prompt"), and log the level distribution per model version to catch a checkpoint that drifts toward revealing output. Log levels and request ids only, never the images.
Regenerate, blur or refuse
The example regenerates once with the same prompt, which often works because sampling is random. Other options: regenerate with a negative prompt appended, return a blurred preview with a reveal button for adult-rated products, or refuse with a message. Whatever you pick, decide it from policy.blocked and keep the generated bytes in memory until that decision is made; writing them to a public bucket first and deleting on block leaves a window where the file was reachable.
Errors
413 media_too_large: 8 MB on the free tier, 32 MB on paid accounts. WebP keeps a 1024 by 1024 image under a megabyte; raw PNG at 2048 can exceed 8 MB, so encode before you send.415 unsupported_mediaor422 decode_failed: the encoder produced something the API cannot read. Check the format string.402 insufficient_credits: the balance is empty. Decide whether generation pauses (safer) or continues unchecked (not recommended); auto-recharge in the console avoids the choice.429 rate_limitedorconcurrency_exceeded: a batch job on the free tier (1 request per second, 2 in flight) needs a queue; paid accounts take 3 per second and 8 in flight. The SDK retries 429 and 503 twice withRetry-Afterfor URL requests; uploads are not retried, so wrap the call in your job's retry.504 timeout: rare for single images; send a smaller encoding.
Failed requests are never charged. Every code is on the errors page.
Cost
One credit per image, so a generator producing 500 images a month runs free; 10,000 credits are $10 and never expire, which is a tenth of a cent per generation. Regenerations cost a credit each, so a prompt that is blocked twice costs two. Images above 10 MB cost one extra credit per started 10 MB; WebP output never gets there. The pricing page has the packs, and the AI image generation use case has the short version of this guide.
Frequently asked questions
- Why not use the safety checker that ships with the pipeline?
- The bundled checkers are a yes/no gate with a fixed idea of unsafe, and they are easy to turn off. A level lets you allow swimwear, block nudity and treat drawings differently, and the decision is made by a service your users cannot patch out.
- Does the check slow generation down?
- Typically by about one second, with the image already in memory; a 1024 by 1024 PNG is a few megabytes and WebP is a few hundred kilobytes. Run it in the same request so a blocked image is never shown.
- Are the generated images stored anywhere?
- No. The API analyses the bytes in memory and discards them when the response leaves. It keeps no bytes, hashes or file names, so a prompt's output never exists outside your pipeline.
- Is one check per image enough for img2img and inpainting?
- Check every output you intend to show. Inpainting a small region can change the level of the whole picture, so the final composite is what should be scored, not the mask or the source.
Related
Next steps
Read the quickstart to get your first result in two minutes, see pricing for the free tier and prepaid packs, or get a free API key and start with your own images.