Skip to content

LycheeOrg/Lychee-NSFW-Classification

Repository files navigation

Lychee NSFW Classification

NSFW content moderation microservice for Lychee.

Analyses photos for explicit content using NudeNet and reports results to the Lychee PHP backend via a REST callback.

Build Status Code Coverage CII Best Practices Summary OpenSSF Scorecard
Website Documentation Changelog Discord


Caution

License — GNU Affero General Public License v3 (AGPL-3.0)

This repository is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).

This is different from all other LycheeOrg repositories, which are released under the MIT License.

Why AGPL-3.0?

This service uses NudeNet, which is itself licensed under the AGPL-3.0. Because AGPL-3.0 is a strong copyleft license, any software that links to or distributes NudeNet must adopt the same license. This entire repository is therefore AGPL-3.0.

See the LICENSE file for the full license text.


How it works

  1. Lychee sends a POST /api/nsfw/detect request with a photo ID and its path on the shared volume.
  2. The service returns 202 Accepted immediately and enqueues the job.
  3. A background worker runs NudeNet inference on the image.
  4. Results are POSTed back to Lychee's callback endpoint (/api/v2/NsfwDetection/results).

The callback payload classifies detections into three tiers:

Field Meaning
should_block One or more detections matched the block label set — hide the photo.
should_review One or more detections matched the review label set — send for human moderation.
is_sensitive One or more detections matched the sensitive label set — mark the photo but keep it visible.

All three tiers are independent: a photo can be both should_block and is_sensitive if labels from both sets are detected.

Tech stack

Concern Library
Web framework FastAPI + Uvicorn
Inference NudeNet v3 (ONNX)
Image loading Pillow
HTTP client httpx
Config Pydantic BaseSettings

Directory layout

Lychee-NSFW-Classification/
├── app/
│   ├── __init__.py
│   ├── main.py            # FastAPI app factory + lifespan
│   ├── config/
│   │   ├── __init__.py    # Re-exports all public names
│   │   ├── labels.py      # VALID_LABELS frozenset
│   │   ├── models.py      # LabelThreshold, LabelSetConfig
│   │   ├── presets.py     # Named presets (strict, moderation, …)
│   │   └── settings.py    # AppSettings (Pydantic BaseSettings)
│   ├── api/
│   │   ├── dependencies.py  # API key authentication
│   │   ├── routes.py        # /detect, /health, /config, /queue
│   │   └── schemas.py       # Pydantic request/response/callback models
│   ├── detection/
│   │   └── detector.py      # NudeNet wrapper + classify()
│   └── queue/
│       ├── base.py          # JobQueue ABC
│       ├── factory.py       # Queue backend selection
│       └── worker.py        # Background job runner
├── tests/
├── docs/                  # Developer documentation (Diátaxis)
├── Dockerfile
├── Makefile
└── pyproject.toml

Quick start — choose a preset

The fastest way to configure the service is to pick a preset that matches your use case:

Preset VISION_NSFW_PRESET= Block Review Sensitive
Strict strict All exposed nudity incl. male chest Covered intimate parts Belly, armpits, feet
Moderation moderation (nothing) All exposed nudity Covered intimate parts
Nude female nude_female Male genitalia, anus Female genitalia Female breast/buttocks + covered parts
Permissive permissive Genitalia + anus only (nothing) Female/male breast, buttocks
Social media social_media Female breast, all genitalia, anus Buttocks, male chest Covered intimate parts

Set it in .env:

VISION_NSFW_PRESET=strict

Individual tier settings (VISION_NSFW_BLOCK, VISION_NSFW_REVIEW, VISION_NSFW_SENSITIVE) always override the preset, so you can start from a preset and refine from there.

You can also fine-tune each preset independently at startup and let callers select a preset per request:

# Raise the confidence bar for the strict preset's block tier
VISION_NSFW_STRICT__BLOCK__CONFIDENCE=0.9

# Require detections to cover ≥ 5 % of the image for nude_female's review tier
VISION_NSFW_NUDE_FEMALE__REVIEW__AREA_RATIO=0.05
{ "photo_id": "42", "photo_path": "2024/01/photo.jpg", "preset": "strict" }

See Choose a preset and Configure classification tiers for details.

Environment variables

All variables are prefixed VISION_NSFW_. Copy .env.example to .env and fill in the required values.

Required

Variable Description
VISION_NSFW_API_KEY Shared secret — validated on inbound requests via X-API-Key and sent on outbound callbacks. Must match AI_VISION_NSFW_API_KEY in Lychee's .env.
VISION_NSFW_LYCHEE_API_URL Lychee base URL for callbacks, no trailing slash. Example: https://lychee.example.com.

Classification

Variable Default Description
VISION_NSFW_PRESET (none) Named preset: strict, moderation, nude_female, permissive, social_media. Populates block/review/sensitive defaults; explicit tier settings override it.
VISION_NSFW_CONFIDENCE_THRESHOLD 0.1 Global fallback minimum confidence (0.0–1.0) for a detection to trigger any tier.
VISION_NSFW_AREA_RATIO_THRESHOLD 0.0 Global fallback minimum area fraction (0.0–1.0) a detection must cover. 0.0 = no area filter.
VISION_NSFW_BLOCK (see defaults) JSON object configuring the block tier. See Configuration Reference.
VISION_NSFW_REVIEW (see defaults) JSON object configuring the review tier.
VISION_NSFW_SENSITIVE (see defaults) JSON object configuring the sensitive tier.
VISION_NSFW_<PRESET>__<TIER>__<FIELD> (none) Per-preset threshold override. Tunes a specific preset in isolation so all presets are ready for per-request selection. Example: VISION_NSFW_STRICT__BLOCK__CONFIDENCE=0.9.

Full reference: docs/3-reference/configuration.md

API endpoints

Base path: /api/nsfw

Method Path Auth Description
GET /api/nsfw/health No Service health check
POST /api/nsfw/detect Yes Enqueue an NSFW detection job
GET /api/nsfw/config Yes Show active configuration (secrets redacted)
GET /api/nsfw/queue Yes Number of pending jobs
DELETE /api/nsfw/queue Yes Purge all pending jobs
GET /api/nsfw/queue/{photo_id} Yes Queue position of a specific job

Interactive docs: http://localhost:8000/docs

POST /api/nsfw/detect — request

{
  "photo_id": "42",
  "photo_path": "2024/01/photo.jpg",
  "preset": "strict"
}
Field Required Description
photo_id Yes Lychee photo identifier, echoed back in the callback.
photo_path Yes Path relative to VISION_NSFW_PHOTOS_PATH. Validated to stay within that root.
preset No Override the service-level preset for this job. See Choose a preset.

The endpoint returns 202 Accepted immediately. Results arrive via callback.

Callback payload (POSTed to {VISION_NSFW_LYCHEE_API_URL}/api/v2/NsfwDetection/results)

{
  "photo_id": "42",
  "status": "success",
  "should_block": true,
  "should_review": false,
  "is_sensitive": true,
  "all_detected": [
    {
      "label": "FEMALE_GENITALIA_EXPOSED",
      "confidence": 0.91,
      "bbox": {"x": 120, "y": 200, "width": 300, "height": 280},
      "area_pixels": 84000,
      "area_ratio": 0.175
    },
    {
      "label": "FEMALE_BREAST_COVERED",
      "confidence": 0.74,
      "bbox": {"x": 50, "y": 80, "width": 150, "height": 140},
      "area_pixels": 21000,
      "area_ratio": 0.044
    }
  ],
  "block_detected": [
    {
      "label": "FEMALE_GENITALIA_EXPOSED",
      "confidence": 0.91,
      "bbox": {"x": 120, "y": 200, "width": 300, "height": 280},
      "area_pixels": 84000,
      "area_ratio": 0.175
    }
  ],
  "review_detected": [],
  "sensitive_detected": [
    {
      "label": "FEMALE_BREAST_COVERED",
      "confidence": 0.74,
      "bbox": {"x": 50, "y": 80, "width": 150, "height": 140},
      "area_pixels": 21000,
      "area_ratio": 0.044
    }
  ]
}

On failure, the callback contains "status": "error" with error_code and message fields.

Full API reference: docs/3-reference/api.md

Development

Setup

# Install uv (https://docs.astral.sh/uv/getting-started/installation/)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install all dependencies (including dev)
uv sync

# Copy and edit the env file
cp .env.example .env

Makefile

make lint          # ruff check + ruff format --check + ty check
make format        # ruff format + ruff check --fix
make test          # pytest
make run           # uvicorn with --reload (local dev)
make docker-build  # docker build
make docker-run    # docker run --env-file .env

The service will be available at http://localhost:8000.

Running tests with coverage

uv run pytest --cov=app --cov-report=html

Docker

# Build
make docker-build

# Run using .env for configuration
make docker-run

# Or manually, mounting the Lychee uploads volume
docker run --rm \
  --env-file .env \
  -v /path/to/lychee/public/uploads:/data/photos:ro \
  -p 8000:8000 \
  lychee-nsfw-classification

See Deploy with Docker for a full deployment guide.

About

No description, website, or topics provided.

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages