Two-stage NSFW moderation for GIFs, videos, and images via local HuggingFace models and/or AWS Rekognition.
Project description
PyFrame
NSFW moderation for GIFs, videos, and images using local HuggingFace models and/or AWS Rekognition.
PyFrame uses temporal segmentation to avoid moderating every frame: it splits an animation into equal time buckets and extracts the most significant frame from each, capturing diverse scene coverage at a fraction of the cost. It also offers an optional two-stage cascade (--prescreen): a free local model soft-screens densely, and only the flagged time windows get escalated to the precise (e.g. AWS) backend. See the pipeline diagram for a visual of the approach.
Install
pip install "pyframe-gif-video-image-moderation[local]" # free local HuggingFace backend
pip install "pyframe-gif-video-image-moderation[aws]" # AWS Rekognition backend
pip install "pyframe-gif-video-image-moderation[all]" # everything (local + aws + video)
Or with uv:
uv add "pyframe-gif-video-image-moderation[local]"
# or, ad-hoc: uv pip install "pyframe-gif-video-image-moderation[local]"
The base install is intentionally light (just opencv-python-headless, numpy, Pillow); the heavy backends (boto3, transformers/torch, moviepy) are optional extras you only pull in if you use them.
Python API
Pipe is the high-level facade: build it, call run().
from pyframe import Pipe
result = Pipe("clip.gif", backend="local").run()
print(result.verdict) # clean
print(result.is_nsfw) # False
Swap the backend, or turn on the two-pass cascade:
Pipe("clip.gif", backend="aws").run() # AWS Rekognition
Pipe("clip.gif", backend="aws", prescreen=True).run() # local screens, AWS confirms
Tuning the two-pass
Every knob is a Pipe param with a sensible default:
Pipe(
"clip.gif",
backend="aws", # precise backend used on escalation
prescreen=True, # two-pass cascade on
escalate_threshold=0.15, # escalate on the faintest local signal (lower = more recall, more cost)
max_escalations=2, # hard cap on AWS calls per file
frames_per_batch=2, # frames merged into each grid sent to AWS
screen_fps=2.0, # soft-screen sample rate
min_confidence=0.5, # NSFW threshold (defaults to the backend's recall-safe value)
).run()
CLI
The same pipeline as a command, no script to edit:
pyframe clip.gif # auto backend, prints a verdict
pyframe clip.gif --backend local # free local model
pyframe clip.gif --backend aws --region us-east-1 # AWS Rekognition
pyframe clip.gif --prescreen --backend aws # cascade: local gate then AWS
pyframe a.gif b.gif c.png --json # batch, machine-readable
Exit code: 0 clean, 1 NSFW (per --fail-on), 2 bad input, 3 backend not installed, so it drops straight into a shell gate: pyframe upload.gif || reject. Equivalent module form: python -m pyframe clip.gif.
Options
| Flag | Default | Meaning |
|---|---|---|
--backend |
auto |
local, aws, or local:<model-id> |
--model |
model default | HuggingFace model id (local backend) |
--region |
us-east-1 |
AWS region (aws backend) |
--max-frames |
10 |
frames to extract from a GIF/video |
--min-confidence |
backend default | NSFW threshold (0-1); 0.5 local, 0.8 aws |
--sampler |
motion |
motion (bucketing) or dense (uniform) |
--prescreen |
off | enable the two-stage cascade |
--escalate-threshold |
0.15 |
cascade gate (low = recall-safe) |
--max-escalations |
2 |
hard cap on precise (AWS) calls per file |
--screen-fps |
2.0 |
soft-screen sample rate |
--use-merged / --frames-per-batch |
off / 2 |
merge frames into a grid before classifying |
--save-frames DIR |
off | write the classified frames to DIR |
--json / --fail-on |
off / nsfw |
output format / exit-code policy |
How it works
Pipe- facade you construct (mirrors the old main.py flow)Scanner- engine: single-pass, or the two-stage cascadeBackend- local (HuggingFace) or aws (Rekognition), normalized resultsSampler- motion bucketing, dense uniform, or suspicion
Single-pass (default): extract max_frames via motion bucketing, then classify each with one backend.
Cascade (--prescreen): a free local model densely soft-screens the whole clip; if any frame scores above --escalate-threshold (a deliberately low recall gate), the most-suspicious frames are merged into grids and sent to the precise backend, capped at --max-escalations calls per file (default 2) so a heavily-flagged clip can never cost more than a single-pass scan. Clean media short-circuits to ~$0 and never hits the expensive backend. Because the soft-screen looks at content (not motion), it won't discard a unique suspicious frame the way motion bucketing can, and it fails open: a decode/inference error escalates rather than silently clearing.
Cost
AWS Rekognition bills ~$1.00 / 1,000 images. A 150-frame GIF costs $0.15 to moderate every frame; PyFrame's 10-bucket extraction drops that to ~$0.01 (a ~93% reduction). With --prescreen, clean clips cost $0 (local only) and flagged clips incur at most --max-escalations AWS calls (default 2), so the cascade never costs more than a single-pass scan.
Tune the cascade on labeled data before relying on it: the local gate's recall bounds the system's recall. Keep
--escalate-thresholdlow (catch anything potentially NSFW) and sample densely enough (--screen-fps) that brief events don't fall between samples.
Pipeline
A 150-frame GIF flows through temporal segmentation down to a handful of extracted frames, optionally merged into grids, then sent to the backend:
Notes
- The
awsbackend needs credentials: install withpip install "pyframe-gif-video-image-moderation[aws]", then runaws configure(or setAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, andAWS_DEFAULT_REGION). [video](video to GIF) needsmoviepy, which requires a system ffmpeg (brew install ffmpeg).- HuggingFace model weights have their own licenses, separate from this package's MIT license.
Development
uv pip install -e ".[dev]" # or: pip install -e ".[dev]"
pytest
python -m build # or: uv build
twine check dist/* # or: uv publish (to PyPI)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyframe_gif_video_image_moderation-0.1.0.tar.gz.
File metadata
- Download URL: pyframe_gif_video_image_moderation-0.1.0.tar.gz
- Upload date:
- Size: 16.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8df602bb72df3e532b475c4cf0fa3e0bb148d701438680b4c50897218cfa3e11
|
|
| MD5 |
13102a0e6e3d3c704c81fd90b1561a25
|
|
| BLAKE2b-256 |
cd40cbb1128cd2c520b62859d4f453bedf700341e7acea42c86a38b56fcfe8d5
|
Provenance
The following attestation bundles were made for pyframe_gif_video_image_moderation-0.1.0.tar.gz:
Publisher:
publish.yml on ehewes/pyframe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyframe_gif_video_image_moderation-0.1.0.tar.gz -
Subject digest:
8df602bb72df3e532b475c4cf0fa3e0bb148d701438680b4c50897218cfa3e11 - Sigstore transparency entry: 1687768798
- Sigstore integration time:
-
Permalink:
ehewes/pyframe@e9ce3067fb981b87089b8eb2750c688539290bcf -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ehewes
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e9ce3067fb981b87089b8eb2750c688539290bcf -
Trigger Event:
release
-
Statement type:
File details
Details for the file pyframe_gif_video_image_moderation-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyframe_gif_video_image_moderation-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
db450a8f055e3e4f37ce52001cdae9d8a095911e9034fa2400ccd58c68a5b6ba
|
|
| MD5 |
366f9ee7a533fdfc4c509ad37e3ed8c6
|
|
| BLAKE2b-256 |
0e50537cdec98f6714e3421de9378a4a02afe0ae315d599f02c29b33cc838c2f
|
Provenance
The following attestation bundles were made for pyframe_gif_video_image_moderation-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ehewes/pyframe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyframe_gif_video_image_moderation-0.1.0-py3-none-any.whl -
Subject digest:
db450a8f055e3e4f37ce52001cdae9d8a095911e9034fa2400ccd58c68a5b6ba - Sigstore transparency entry: 1687768858
- Sigstore integration time:
-
Permalink:
ehewes/pyframe@e9ce3067fb981b87089b8eb2750c688539290bcf -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ehewes
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e9ce3067fb981b87089b8eb2750c688539290bcf -
Trigger Event:
release
-
Statement type: