Skip to content

NSFW Classification

Lychee can automatically scan uploaded photos for explicit content and react based on how much you trust the uploader: silently hard-delete it, hold it for admin review, mark the containing album as sensitive, or just log the finding and let it through. Detection runs in a dedicated Lychee-NSFW-Classification microservice using NudeNet, kept separate from the main PHP application.

  1. When a photo is uploaded (or rescanned), Lychee sends the photo to the NSFW classification service.
  2. NudeNet inference runs in the background and the service classifies any findings into three independent tiers:
    • Block — hide or delete the photo.
    • Review — send the photo for human moderation.
    • Sensitive — keep the photo visible, but mark its album(s) as sensitive.
  3. A photo can match more than one tier at once (e.g. both block and sensitive), and what actually happens depends on the uploader’s trust level — see below.

Lychee already assigns each user an upload trust level (check, monitor, trust_but_verify, trusted). NSFW outcomes are decided per trust level, mostly via Settings:

Trust levelBlock findingReview findingSensitive finding
CheckConfigurable, default block (the photo is hard-deleted)Always held for reviewHeld for review (album action deferred to approval)
MonitorConfigurable, default moderate (held for admin review)Always held for reviewConfigurable (default: mark the album(s) as sensitive)
Trust-but-verifyConfigurable, default moderateAuto-approvedConfigurable (default: mark the album(s) as sensitive)
TrustedConfigurable, default approve (logged only — and only scanned at all if ai_vision_nsfw_scan_trusted_users is on)Auto-approvedConfigurable (default: mark the album(s) as sensitive)

For Monitor, Trust-but-verify, and Trusted users, if a sensitive finding fires on a photo with no album (unsorted), the fallback is controlled separately by ai_vision_nsfw_sensitive_no_album_action (skip with a warning, or hold for review).

When a photo’s sensitive finding marks its album, the album is flagged internally (is_nsfw); a parent album that’s already flagged isn’t re-flagged. Visiting a sensitive album shows a dismissable content warning overlay before revealing its contents — admins can customize the warning text and whether the backdrop is blurred or solid.

The feature is controlled by the same AI Vision category of Settings as facial recognition, all conservative by default:

SettingDescriptionDefault
ai_vision_nsfw_enabledEnable NSFW classification. Requires ai_vision_enabled.off
ai_vision_nsfw_presetDetection preset sent to the classifier, see Presets.default
ai_vision_nsfw_check_block_actionBlock-finding action for Check users: block or moderate.block
ai_vision_nsfw_monitor_block_actionBlock-finding action for Monitor users: block or moderate.moderate
ai_vision_nsfw_trust_but_verify_block_actionBlock-finding action for Trust-but-verify users: block or moderate.moderate
ai_vision_nsfw_trust_block_actionBlock-finding action for Trusted users: block, moderate, or approve.approve
ai_vision_nsfw_sensitive_album_actionWhether sensitive findings mark the photo’s album(s): mark_album or nothing.mark_album
ai_vision_nsfw_sensitive_no_album_actionFallback for a sensitive finding on an unsorted photo: skip or moderate.skip
ai_vision_nsfw_scan_trusted_usersAlso scan photos uploaded by Trusted users.off
ai_vision_nsfw_monitor_hide_on_scanTemporarily hide Monitor-tier photos while the scan is in progress.off
ai_vision_nsfw_trust_but_verify_hide_on_scanTemporarily hide Trust-but-verify photos while the scan is in progress.off
ai_vision_nsfw_trust_hide_on_scanTemporarily hide Trusted photos while the scan is in progress.off

For the *_hide_on_scan settings: if the classification service is unavailable, the photo stays hidden until manually approved.

ai_vision_nsfw_preset (and the service-side VISION_NSFW_PRESET, see below) selects a named bundle of block/review/sensitive labels:

PresetBlockReviewSensitive
StrictAll exposed nudity, including male chestCovered intimate partsBelly, armpits, feet
Moderation(nothing)All exposed nudityCovered intimate parts
Nude femaleMale genitalia, anusFemale genitaliaFemale breast/buttocks + covered parts
PermissiveGenitalia + anus only(nothing)Female/male breast, buttocks
Social mediaFemale breast, all genitalia, anusButtocks, male chestCovered intimate parts

The default preset (Lychee’s own default) uses the service’s built-in defaults: block on exposed genitalia/anus, review on exposed buttocks/female breast, and flag covered intimate parts + exposed belly as sensitive.

NSFW classification requires running the Lychee-NSFW-Classification service (FastAPI + NudeNet) alongside Lychee — remember it is AGPL-3.0 licensed, see above — then pointing Lychee at it via the AI_VISION_NSFW_URL and AI_VISION_NSFW_API_KEY environment variables — see AI Vision in the configuration reference. The diagnostics page (admins only) reports whether the service is reachable and correctly configured.

The service is configured independently, via its own .env file (copy .env.example to .env). All of its variables are prefixed VISION_NSFW_.

VariableDescription
VISION_NSFW_API_KEYShared secret, validated via the X-API-Key header on inbound requests and sent on outbound callbacks. Must match AI_VISION_NSFW_API_KEY in Lychee’s own .env. Do not leave empty in production.
VISION_NSFW_LYCHEE_API_URLLychee base URL for callbacks, no trailing slash (e.g. https://lychee.example.com).
VariableDefaultDescription
VISION_NSFW_VERIFY_SSLtrueVerify SSL certificates on outbound callbacks. Don’t disable in production.
VISION_NSFW_SKIP_LYCHEE_CHECKfalseSkip the Lychee connectivity check at startup.
VISION_NSFW_PHOTOS_PATH/data/photosShared volume mount the service reads photo files from. Mount Lychee’s LYCHEE_UPLOADS directory here, read-only. Requested photo_path values are validated to stay within this root.
VariableDefaultDescription
VISION_NSFW_PRESETnoneLoad a named preset (strict, moderation, nude_female, permissive, social_media) as the service default. Explicit tier settings below override it; a per-request preset field overrides this entirely.
VISION_NSFW_CONFIDENCE_THRESHOLD0.1Global fallback minimum confidence (0.0–1.0) for any tier.
VISION_NSFW_AREA_RATIO_THRESHOLD0.0Global fallback minimum fraction of image area a detection must cover. 0.0 disables the filter.
VISION_NSFW_BLOCK / _REVIEW / _SENSITIVEservice defaultsJSON object configuring that tier’s labels/thresholds, e.g. VISION_NSFW_BLOCK='{"labels": [...], "confidence": 0.7}'. Individual fields can also be set with __ sub-keys, e.g. VISION_NSFW_BLOCK__CONFIDENCE=0.7.
VISION_NSFW_<PRESET>__<TIER>__<FIELD>noneTune a specific preset in isolation (e.g. VISION_NSFW_STRICT__BLOCK__CONFIDENCE=0.9), so every preset is ready for per-request selection regardless of the service-level default.

This service supports much finer-grained tuning (per-label thresholds, area-ratio filters, replacing a preset’s label list) than is exposed here — see the service’s own configuration reference for the full set of options.

VariableDefaultDescription
VISION_NSFW_QUEUE_BACKENDdatabasedatabase (SQLite) or redis.
VISION_NSFW_QUEUE_MAX_SIZE0Maximum pending jobs. 0 = unlimited; beyond it, requests get 429 Too Many Requests.
VISION_NSFW_STORAGE_PATH/data/queueDirectory for the SQLite queue database (database backend only).
VISION_NSFW_REDIS_HOSTlocalhostRedis host (redis backend only).
VISION_NSFW_REDIS_PORT6379Redis port.
VISION_NSFW_REDIS_PASSWORDemptyRedis password.
VISION_NSFW_REDIS_DB0Redis logical database index.
VariableDefaultDescription
VISION_NSFW_THREAD_POOL_SIZE1Threads for CPU-bound NudeNet inference. Only raise if your NudeNet build is confirmed thread-safe.
VISION_NSFW_WORKERS1Uvicorn worker processes. Prefer multiple container replicas over raising this for throughput.
VISION_NSFW_LOG_LEVELinfodebug, info, warning, error, or critical.
Terminal window
docker run --rm \
--env-file .env \
-v /path/to/lychee/public/uploads:/data/photos:ro \
-p 8000:8000 \
lychee-nsfw-classification

The container exposes interactive API docs at /docs and a health check at /api/nsfw/health once running.