Skip to content
Arrit Gashi
← All work
Shipped2026Machine learning · Developer tool

ModerationKit

Self-hosted image moderation that runs offline, and corrects a documented bias in the model it depends on.

The ModerationKit interface: badges naming the NudeNet 640m detector and the Falconsai classifier, a beach photograph of a woman in swimwear as the input, and a green ALLOW verdict reading whole-image NSFW 0.12 with no explicit body parts above threshold, the classifier at 11.7 percent NSFW against 88.3 percent safe, and 8 detections of which 5 are shown.
Swimwear is the case the covered-versus-exposed logic exists for. Five confident detections, every one categorised suggestive, verdict ALLOW.
score boost correcting a ~3× gender bias
2.5×
detector model, not the 12 MB default
104 MB
framework integrations shipped
4

The problem

Every free open-source nudity detector is a thin wrapper around NudeNet behind a single threshold. NudeNet's training data is female-skewed, so male anatomy scores roughly three times lower than the equivalent female detection, and the model will sometimes assert the wrong-gender class over the same region of an image. Wrapping that raw means shipping its false negatives to production. The paid alternatives fix the accuracy by charging per image and requiring you to upload your users' photos to someone else's server.

What I built

Two models behind one verdict. A NudeNet body-part detector — the 104 MB 640m ONNX weights rather than the 12 MB nano model the pip package bundles — provides the annotations, and a Falconsai vision transformer classifies the image as a whole. A composition layer runs both, applies a three-stage correction pipeline, and returns a single ALLOW, REVIEW or BLOCK.

Three delivery surfaces off the same core: a FastAPI service with a /check endpoint and Swagger docs, a Gradio UI for humans, and a batch CLI for folders. Every threshold is an environment variable with a documented default. Docker Compose brings up the API and the UI from one image with a healthcheck, and there are one-command installers for Windows, macOS and Linux.

The README is treated as part of the product. It ships the verdict semantics, a threshold table, paste-ready fail-closed integrations for Express, Next.js App Router, Django and Laravel, an explicit bias notice, and a responsible-use section that refuses the CSAM use case outright and routes operators to PhotoDNA, Thorn and the NCMEC CyberTipline instead.

The hard part

Demoting the model you built on

The correction is three mechanisms applied in a fixed order. Male-class raw scores are multiplied by 2.5 and clamped, because they peak around 0.2–0.4 where the female equivalents peak at 0.7–0.9. A confident face-gender detection — trusted only at or above 0.75, and skipped entirely when both genders appear — suppresses the anatomically contradictory classes. And when two biologically impossible detections overlap at an IoU of 0.5 or more, both are dropped rather than keeping the higher scorer, on the reasoning that a biased detector gives you no principled way to pick the winner.

The decision underneath all three is the one that mattered: the whole-image classifier was promoted to the primary verdict signal and the detector demoted to an annotation source. The detector answers "what specifically is in this image", which is what it is good at; it no longer decides whether the image is a problem, which is what the bias corrupts. The documentation says plainly that this is compensation and not a fix — no amount of post-processing repairs a biased model, and claiming otherwise would be the more dangerous bug.

Outcome

  • Public and MIT-licensed, running offline with no per-image cost and no image ever leaving the operator's machine.
  • A documented bias-compensation pipeline that raw-NudeNet projects do not have: a 2.5× score boost, a 0.75-gated face suppression, and IoU-based conflict resolution.
  • Four paste-ready integrations and nine environment-tunable thresholds, so adopting it is a copy rather than a port.
  • A responsible-use section that turns away the one use case the tool must not serve, and names the organisations that should handle it instead.

Screens

  • A BLOCK verdict panel: a red banner reading BLOCK with the note whole-image NSFW score 1.00 is at or above 0.7, returned in 2372 milliseconds, beside cards showing the classifier at 100 percent NSFW against 0 percent safe, a top explicit detection of FEMALE_BREAST_EXPOSED scoring 0.874, and 8 detections of which 6 survived filtering.

    The other end of the same scale. The whole-image classifier is the primary signal, so 1.00 against a 0.7 threshold blocks outright. The image that produced this is not shown.

  • The detection table for that blocked image: six rows of class, category, score and pixel size. Two FEMALE_BREAST_EXPOSED rows are categorised explicit at 0.874 and 0.867; BELLY_EXPOSED, two ARMPITS_EXPOSED and FEET_EXPOSED are categorised suggestive, between 0.804 and 0.475.

    Why that verdict is defensible: the detector classifies each region separately. Only the two explicit rows force a block. The same table on the cover image returns suggestive throughout.

  • Generated Swagger documentation for the ModerationKit API listing GET /health, GET /policy and POST /check, with /check expanded to show a multipart upload taking a file field and returning a moderation verdict with scores and detections.

    The API an integrator actually adopts: one multipart POST, returning the verdict, both scores and every detection.

  • A terminal running the batch command over a folder of five images: four return CLEAN with zero hits, the fifth returns SUGGESTIVE with six hits, and a report is written to report.csv.

    The third surface, for moderating a library that already exists rather than a live upload. One folder, one verdict per file, one CSV.

Want something like this built? I take on a small number of projects at a time.