openmoderate 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openmoderate-1.0.1/.dockerignore +13 -0
- openmoderate-1.0.1/.github/ISSUE_TEMPLATE/bug_report.yml +29 -0
- openmoderate-1.0.1/.github/ISSUE_TEMPLATE/config.yml +5 -0
- openmoderate-1.0.1/.github/ISSUE_TEMPLATE/misclassification.yml +39 -0
- openmoderate-1.0.1/.github/dependabot.yml +20 -0
- openmoderate-1.0.1/.github/pull_request_template.md +15 -0
- openmoderate-1.0.1/.github/workflows/ci.yml +66 -0
- openmoderate-1.0.1/.github/workflows/docker.yml +90 -0
- openmoderate-1.0.1/.github/workflows/docs.yml +41 -0
- openmoderate-1.0.1/.github/workflows/release.yml +84 -0
- openmoderate-1.0.1/.gitignore +17 -0
- openmoderate-1.0.1/CHANGELOG.md +134 -0
- openmoderate-1.0.1/CITATION.cff +17 -0
- openmoderate-1.0.1/CODE_OF_CONDUCT.md +14 -0
- openmoderate-1.0.1/CONTRIBUTING.md +62 -0
- openmoderate-1.0.1/Dockerfile +34 -0
- openmoderate-1.0.1/LICENSE +202 -0
- openmoderate-1.0.1/PKG-INFO +164 -0
- openmoderate-1.0.1/README.md +109 -0
- openmoderate-1.0.1/SECURITY.md +39 -0
- openmoderate-1.0.1/benchmark/README.md +77 -0
- openmoderate-1.0.1/benchmark/RESULTS.md +38 -0
- openmoderate-1.0.1/benchmark/manifest.csv +483 -0
- openmoderate-1.0.1/benchmark/results.json +394 -0
- openmoderate-1.0.1/benchmark/run.py +191 -0
- openmoderate-1.0.1/docs/assets/demo.svg +85 -0
- openmoderate-1.0.1/docs/assets/how-it-works.svg +149 -0
- openmoderate-1.0.1/docs/assets/social-preview.png +0 -0
- openmoderate-1.0.1/docs/assets/social-preview.svg +25 -0
- openmoderate-1.0.1/docs/benchmarks.md +56 -0
- openmoderate-1.0.1/docs/changelog.md +1 -0
- openmoderate-1.0.1/docs/guide/cli.md +12 -0
- openmoderate-1.0.1/docs/guide/concepts.md +46 -0
- openmoderate-1.0.1/docs/guide/integrations.md +82 -0
- openmoderate-1.0.1/docs/guide/python.md +63 -0
- openmoderate-1.0.1/docs/guide/server.md +66 -0
- openmoderate-1.0.1/docs/guide/training.md +45 -0
- openmoderate-1.0.1/docs/guide/video.md +36 -0
- openmoderate-1.0.1/docs/index.md +56 -0
- openmoderate-1.0.1/docs/reference.md +51 -0
- openmoderate-1.0.1/docs/requirements.txt +3 -0
- openmoderate-1.0.1/docs/security.md +1 -0
- openmoderate-1.0.1/examples/django_app.py +56 -0
- openmoderate-1.0.1/examples/fastapi_app.py +32 -0
- openmoderate-1.0.1/examples/flask_app.py +27 -0
- openmoderate-1.0.1/examples/policy.yaml +14 -0
- openmoderate-1.0.1/examples/quickstart.py +22 -0
- openmoderate-1.0.1/mkdocs.yml +64 -0
- openmoderate-1.0.1/pyproject.toml +75 -0
- openmoderate-1.0.1/scripts/changelog_section.py +29 -0
- openmoderate-1.0.1/scripts/make_readme_graphics.py +426 -0
- openmoderate-1.0.1/scripts/release_notes.py +76 -0
- openmoderate-1.0.1/src/openmoderate/__init__.py +43 -0
- openmoderate-1.0.1/src/openmoderate/categories.py +203 -0
- openmoderate-1.0.1/src/openmoderate/cli.py +390 -0
- openmoderate-1.0.1/src/openmoderate/config.py +100 -0
- openmoderate-1.0.1/src/openmoderate/engine/__init__.py +4 -0
- openmoderate-1.0.1/src/openmoderate/engine/base.py +19 -0
- openmoderate-1.0.1/src/openmoderate/engine/clip_engine.py +83 -0
- openmoderate-1.0.1/src/openmoderate/integrations/__init__.py +26 -0
- openmoderate-1.0.1/src/openmoderate/integrations/_common.py +176 -0
- openmoderate-1.0.1/src/openmoderate/integrations/django.py +189 -0
- openmoderate-1.0.1/src/openmoderate/integrations/fastapi.py +172 -0
- openmoderate-1.0.1/src/openmoderate/integrations/flask.py +149 -0
- openmoderate-1.0.1/src/openmoderate/io.py +115 -0
- openmoderate-1.0.1/src/openmoderate/moderator.py +274 -0
- openmoderate-1.0.1/src/openmoderate/py.typed +0 -0
- openmoderate-1.0.1/src/openmoderate/result.py +58 -0
- openmoderate-1.0.1/src/openmoderate/server/__init__.py +1 -0
- openmoderate-1.0.1/src/openmoderate/server/app.py +257 -0
- openmoderate-1.0.1/src/openmoderate/server/fetch.py +86 -0
- openmoderate-1.0.1/src/openmoderate/training/__init__.py +12 -0
- openmoderate-1.0.1/src/openmoderate/training/dataset.py +93 -0
- openmoderate-1.0.1/src/openmoderate/training/head.py +82 -0
- openmoderate-1.0.1/src/openmoderate/training/trainer.py +148 -0
- openmoderate-1.0.1/src/openmoderate/video.py +326 -0
- openmoderate-1.0.1/tests/conftest.py +142 -0
- openmoderate-1.0.1/tests/test_cli.py +58 -0
- openmoderate-1.0.1/tests/test_integration_django.py +89 -0
- openmoderate-1.0.1/tests/test_integration_fastapi.py +80 -0
- openmoderate-1.0.1/tests/test_integration_flask.py +85 -0
- openmoderate-1.0.1/tests/test_integrations_common.py +68 -0
- openmoderate-1.0.1/tests/test_moderator.py +138 -0
- openmoderate-1.0.1/tests/test_real_model.py +14 -0
- openmoderate-1.0.1/tests/test_scripts.py +95 -0
- openmoderate-1.0.1/tests/test_server.py +167 -0
- openmoderate-1.0.1/tests/test_training.py +91 -0
- openmoderate-1.0.1/tests/test_video.py +106 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something crashes, errors or behaves differently from the docs
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened?
|
|
9
|
+
description: What did you do, what did you expect, and what happened instead?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: repro
|
|
14
|
+
attributes:
|
|
15
|
+
label: Minimal reproduction
|
|
16
|
+
description: Code or commands we can run. Use a harmless placeholder image.
|
|
17
|
+
render: python
|
|
18
|
+
- type: input
|
|
19
|
+
id: version
|
|
20
|
+
attributes:
|
|
21
|
+
label: openmoderate version
|
|
22
|
+
placeholder: python -c "import openmoderate; print(openmoderate.__version__)"
|
|
23
|
+
validations:
|
|
24
|
+
required: true
|
|
25
|
+
- type: input
|
|
26
|
+
id: env
|
|
27
|
+
attributes:
|
|
28
|
+
label: Python version and OS
|
|
29
|
+
placeholder: Python 3.11, Ubuntu 24.04
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Misclassification (false positive / missed content)
|
|
2
|
+
description: The model flagged something harmless, or missed something it should catch
|
|
3
|
+
labels: [accuracy]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
**Never attach or link harmful images** (nudity, gore, abuse, extremist material)
|
|
9
|
+
in this public tracker. Describe them in words instead. For harmless false
|
|
10
|
+
positives (e.g. a kitchen knife flagged as a weapon), attaching the image is
|
|
11
|
+
welcome if you have the right to share it.
|
|
12
|
+
- type: dropdown
|
|
13
|
+
id: kind
|
|
14
|
+
attributes:
|
|
15
|
+
label: Kind
|
|
16
|
+
options:
|
|
17
|
+
- False positive (harmless content was flagged)
|
|
18
|
+
- Missed content (violating content was not flagged)
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
- type: input
|
|
22
|
+
id: category
|
|
23
|
+
attributes:
|
|
24
|
+
label: Category
|
|
25
|
+
placeholder: e.g. weapons
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
- type: textarea
|
|
29
|
+
id: description
|
|
30
|
+
attributes:
|
|
31
|
+
label: Describe the image
|
|
32
|
+
description: What does the image show? Include the scores from result.to_dict().
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: textarea
|
|
36
|
+
id: pattern
|
|
37
|
+
attributes:
|
|
38
|
+
label: Is it systematic?
|
|
39
|
+
description: Does it happen across many similar images? Have you tried a policy change (thresholds, negatives) or a trained head?
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: github-actions
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: monthly
|
|
7
|
+
groups:
|
|
8
|
+
actions:
|
|
9
|
+
patterns: ["*"]
|
|
10
|
+
- package-ecosystem: pip
|
|
11
|
+
directory: /
|
|
12
|
+
schedule:
|
|
13
|
+
interval: monthly
|
|
14
|
+
groups:
|
|
15
|
+
python:
|
|
16
|
+
patterns: ["*"]
|
|
17
|
+
- package-ecosystem: docker
|
|
18
|
+
directory: /
|
|
19
|
+
schedule:
|
|
20
|
+
interval: monthly
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## What does this change?
|
|
2
|
+
|
|
3
|
+
<!-- One or two sentences, and link the issue it addresses. -->
|
|
4
|
+
|
|
5
|
+
## How was it tested?
|
|
6
|
+
|
|
7
|
+
<!-- Commands you ran; for prompt or threshold changes, before/after numbers from
|
|
8
|
+
`python benchmark/run.py` or your own evaluation set (describe it, don't commit it). -->
|
|
9
|
+
|
|
10
|
+
## Checklist
|
|
11
|
+
|
|
12
|
+
- [ ] `ruff check`, `ruff format --check`, `mypy src/openmoderate` and `pytest` pass
|
|
13
|
+
- [ ] Tests added or updated for behaviour changes
|
|
14
|
+
- [ ] `CHANGELOG.md` updated under "Unreleased"
|
|
15
|
+
- [ ] No harmful images committed (tests use synthetic images)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, "release/**"]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: actions/setup-python@v7
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install (CPU-only torch)
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install --upgrade pip
|
|
23
|
+
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
|
|
24
|
+
pip install -e ".[dev]" pytest-github-actions-annotate-failures
|
|
25
|
+
- name: Check the model stack imports (catches torch/torchvision mismatches)
|
|
26
|
+
run: python -c "import open_clip, torchvision; print(open_clip.__version__, torchvision.__version__)"
|
|
27
|
+
- run: ruff check src tests scripts examples benchmark
|
|
28
|
+
- run: ruff format --check src tests scripts examples benchmark
|
|
29
|
+
- name: Type check
|
|
30
|
+
run: |
|
|
31
|
+
pip install "mypy>=1.10" types-PyYAML
|
|
32
|
+
mypy src/openmoderate --ignore-missing-imports
|
|
33
|
+
- run: pytest -q
|
|
34
|
+
- run: python -m build
|
|
35
|
+
|
|
36
|
+
docker:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
- name: Build image
|
|
41
|
+
run: |
|
|
42
|
+
set -o pipefail
|
|
43
|
+
if ! docker build --progress=plain -t openmoderate:ci . 2>&1 | tee build.log; then
|
|
44
|
+
msg=$(tail -c 4000 build.log | python3 -c "import sys; print(sys.stdin.read().replace('%','%25').replace('\\r','').replace('\\n','%0A'))")
|
|
45
|
+
echo "::error title=docker build failed::$msg"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
- name: Smoke test the container
|
|
49
|
+
run: |
|
|
50
|
+
docker run -d --name om -p 8000:8000 -e OPENMODERATE_API_KEY=ci-key openmoderate:ci
|
|
51
|
+
for i in $(seq 1 60); do curl -sf localhost:8000/health && break; sleep 2; done
|
|
52
|
+
curl -sf localhost:8000/health | grep '"model_loaded":true'
|
|
53
|
+
python3 -c "import zlib,struct; raw=b''.join(b'\x00'+b'\x40\x90\xd0'*64 for _ in range(64)); c=lambda t,d: struct.pack('>I',len(d))+t+d+struct.pack('>I',zlib.crc32(t+d)); open('blue.png','wb').write(b'\x89PNG\r\n\x1a\n'+c(b'IHDR',struct.pack('>IIBBBBB',64,64,8,2,0,0,0))+c(b'IDAT',zlib.compress(raw))+c(b'IEND',b''))"
|
|
54
|
+
test "$(curl -s -o /dev/null -w '%{http_code}' -F file=@blue.png localhost:8000/v1/moderate)" = 401
|
|
55
|
+
curl -sf -H 'X-API-Key: ci-key' -F file=@blue.png localhost:8000/v1/moderate | tee out.json
|
|
56
|
+
grep '"is_safe":true' out.json
|
|
57
|
+
# Video support works end to end: build a 3 s clip inside the container (which has
|
|
58
|
+
# OpenCV) and moderate it. Without OpenCV the endpoint would return 501.
|
|
59
|
+
docker exec om python -c "import cv2, numpy as np; w = cv2.VideoWriter('/tmp/v.mp4', cv2.VideoWriter_fourcc(*'mp4v'), 10, (64, 64)); [w.write(np.full((64, 64, 3), (208, 144, 64), np.uint8)) for _ in range(30)]; w.release()"
|
|
60
|
+
docker cp om:/tmp/v.mp4 v.mp4
|
|
61
|
+
curl -sf -H 'X-API-Key: ci-key' -F file=@v.mp4 localhost:8000/v1/moderate/video | tee video.json
|
|
62
|
+
grep '"frames_analyzed":3' video.json
|
|
63
|
+
grep '"segments":\[\]' video.json
|
|
64
|
+
- name: Container logs
|
|
65
|
+
if: always()
|
|
66
|
+
run: docker logs om || true
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: Docker image
|
|
2
|
+
|
|
3
|
+
# Publishes ghcr.io/<owner>/openmoderate:<version>, :<major>.<minor> and :latest.
|
|
4
|
+
# Runs on every version tag; run it manually ("Run workflow") to (re)publish an existing tag.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
tag:
|
|
12
|
+
description: "Existing tag to publish, e.g. v1.0.0"
|
|
13
|
+
required: true
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
packages: write
|
|
18
|
+
|
|
19
|
+
env:
|
|
20
|
+
REF: ${{ inputs.tag || github.ref_name }}
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
publish:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
|
+
with:
|
|
28
|
+
ref: ${{ env.REF }}
|
|
29
|
+
|
|
30
|
+
- name: Read the package version
|
|
31
|
+
id: version
|
|
32
|
+
run: |
|
|
33
|
+
version=$(python3 -c "import re; print(re.search(r'__version__ = \"(.+?)\"', open('src/openmoderate/__init__.py').read()).group(1))")
|
|
34
|
+
if [ "v$version" != "$REF" ]; then
|
|
35
|
+
echo "::error::Tag $REF does not match package version $version"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
39
|
+
echo "minor=${version%.*}" >> "$GITHUB_OUTPUT"
|
|
40
|
+
|
|
41
|
+
- id: meta
|
|
42
|
+
uses: docker/metadata-action@v6
|
|
43
|
+
with:
|
|
44
|
+
images: ghcr.io/${{ github.repository_owner }}/openmoderate
|
|
45
|
+
tags: |
|
|
46
|
+
type=raw,value=${{ steps.version.outputs.version }}
|
|
47
|
+
type=raw,value=${{ steps.version.outputs.minor }}
|
|
48
|
+
type=raw,value=latest
|
|
49
|
+
labels: |
|
|
50
|
+
org.opencontainers.image.title=openmoderate
|
|
51
|
+
org.opencontainers.image.description=Open-source, self-hosted image and video content moderation REST API
|
|
52
|
+
org.opencontainers.image.licenses=Apache-2.0
|
|
53
|
+
org.opencontainers.image.version=${{ steps.version.outputs.version }}
|
|
54
|
+
|
|
55
|
+
- uses: docker/setup-buildx-action@v4
|
|
56
|
+
|
|
57
|
+
- name: Build
|
|
58
|
+
uses: docker/build-push-action@v7
|
|
59
|
+
with:
|
|
60
|
+
context: .
|
|
61
|
+
load: true
|
|
62
|
+
tags: openmoderate:test
|
|
63
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
64
|
+
cache-from: type=gha
|
|
65
|
+
cache-to: type=gha,mode=max
|
|
66
|
+
|
|
67
|
+
- name: Smoke test the image before publishing
|
|
68
|
+
run: |
|
|
69
|
+
docker run -d --name om -p 8000:8000 -e OPENMODERATE_API_KEY=ci-key openmoderate:test
|
|
70
|
+
for i in $(seq 1 60); do curl -sf localhost:8000/health && break; sleep 2; done
|
|
71
|
+
curl -sf localhost:8000/health | grep '"model_loaded":true'
|
|
72
|
+
docker exec om python -c "from PIL import Image; Image.new('RGB', (64, 64), (64, 144, 208)).save('/tmp/blue.png')"
|
|
73
|
+
docker cp om:/tmp/blue.png blue.png
|
|
74
|
+
curl -sf -H 'X-API-Key: ci-key' -F file=@blue.png localhost:8000/v1/moderate | grep '"is_safe":true'
|
|
75
|
+
docker rm -f om
|
|
76
|
+
|
|
77
|
+
- uses: docker/login-action@v4
|
|
78
|
+
with:
|
|
79
|
+
registry: ghcr.io
|
|
80
|
+
username: ${{ github.actor }}
|
|
81
|
+
password: ${{ github.token }}
|
|
82
|
+
|
|
83
|
+
- name: Publish
|
|
84
|
+
uses: docker/build-push-action@v7
|
|
85
|
+
with:
|
|
86
|
+
context: .
|
|
87
|
+
push: true
|
|
88
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
89
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
90
|
+
cache-from: type=gha
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: actions/setup-python@v7
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.11"
|
|
20
|
+
- run: pip install -r docs/requirements.txt
|
|
21
|
+
- name: Build (strict, so broken links fail the build)
|
|
22
|
+
run: mkdocs build --strict
|
|
23
|
+
- uses: actions/upload-pages-artifact@v5
|
|
24
|
+
with:
|
|
25
|
+
path: site/
|
|
26
|
+
|
|
27
|
+
deploy:
|
|
28
|
+
# One-time setup: Settings -> Pages -> Source: "GitHub Actions", then set the repository
|
|
29
|
+
# variable DOCS_DEPLOY=true. Until then the site is only built (and checked), not deployed.
|
|
30
|
+
if: vars.DOCS_DEPLOY == 'true' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
permissions:
|
|
34
|
+
pages: write
|
|
35
|
+
id-token: write
|
|
36
|
+
environment:
|
|
37
|
+
name: github-pages
|
|
38
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
39
|
+
steps:
|
|
40
|
+
- id: deployment
|
|
41
|
+
uses: actions/deploy-pages@v5
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Push a version tag (git tag v1.2.3 && git push --tags) to:
|
|
4
|
+
# 1. check the tag matches the package version,
|
|
5
|
+
# 2. build the sdist and wheel,
|
|
6
|
+
# 3. create the GitHub release with notes from CHANGELOG.md (and backfill release pages
|
|
7
|
+
# for any older tags that lack one),
|
|
8
|
+
# 4. publish to PyPI through trusted publishing (no tokens stored in the repository).
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
tags: ["v*"]
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
- uses: actions/setup-python@v7
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.11"
|
|
25
|
+
- name: Check the tag matches the package version
|
|
26
|
+
run: |
|
|
27
|
+
version=$(python -c "import re; print(re.search(r'__version__ = \"(.+?)\"', open('src/openmoderate/__init__.py').read()).group(1))")
|
|
28
|
+
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
|
|
29
|
+
echo "::error::Tag $GITHUB_REF_NAME does not match package version $version"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
- name: Check the changelog has an entry for this version
|
|
33
|
+
run: python scripts/changelog_section.py "$GITHUB_REF_NAME" > /dev/null
|
|
34
|
+
- run: pip install build && python -m build
|
|
35
|
+
- uses: actions/upload-artifact@v7
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
|
|
40
|
+
github-release:
|
|
41
|
+
needs: build
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
permissions:
|
|
44
|
+
contents: write
|
|
45
|
+
env:
|
|
46
|
+
GH_TOKEN: ${{ github.token }}
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v7
|
|
49
|
+
with:
|
|
50
|
+
fetch-depth: 0
|
|
51
|
+
- uses: actions/download-artifact@v8
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
55
|
+
- name: Create the release for this tag
|
|
56
|
+
run: |
|
|
57
|
+
python scripts/release_notes.py "$GITHUB_REF_NAME" > notes.md
|
|
58
|
+
if gh release view "$GITHUB_REF_NAME" > /dev/null 2>&1; then
|
|
59
|
+
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
|
|
60
|
+
gh release edit "$GITHUB_REF_NAME" --notes-file notes.md
|
|
61
|
+
else
|
|
62
|
+
gh release create "$GITHUB_REF_NAME" dist/* --title "$GITHUB_REF_NAME" --notes-file notes.md --verify-tag
|
|
63
|
+
fi
|
|
64
|
+
- name: Backfill release pages for older tags
|
|
65
|
+
run: |
|
|
66
|
+
for tag in $(git tag --list 'v*' --sort=version:refname); do
|
|
67
|
+
[ "$tag" = "$GITHUB_REF_NAME" ] && continue
|
|
68
|
+
gh release view "$tag" > /dev/null 2>&1 && continue
|
|
69
|
+
python scripts/changelog_section.py "$tag" > old.md || continue
|
|
70
|
+
gh release create "$tag" --title "$tag" --notes-file old.md --verify-tag --latest=false
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
pypi:
|
|
74
|
+
needs: build
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
environment: pypi
|
|
77
|
+
permissions:
|
|
78
|
+
id-token: write # PyPI trusted publishing
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/download-artifact@v8
|
|
81
|
+
with:
|
|
82
|
+
name: dist
|
|
83
|
+
path: dist/
|
|
84
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/) and the project uses [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [1.0.1] - 2026-10-14
|
|
8
|
+
|
|
9
|
+
First release on PyPI: `pip install openmoderate`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Docker image published to `ghcr.io/karammm/openmoderate` on every release, smoke-tested
|
|
13
|
+
before it is pushed.
|
|
14
|
+
- Release pages now start with copy-paste install commands and link to the docs,
|
|
15
|
+
benchmarks and the full diff.
|
|
16
|
+
- README badges, `CITATION.cff`, `CODE_OF_CONDUCT.md`, Dependabot updates and a social
|
|
17
|
+
preview card.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- GitHub Actions updated to their current major versions.
|
|
21
|
+
- Releases publish to PyPI through trusted publishing on every version tag.
|
|
22
|
+
|
|
23
|
+
## [1.0.0] - 2026-10-14
|
|
24
|
+
|
|
25
|
+
First stable release. The public API (everything importable from `openmoderate`, the
|
|
26
|
+
integration modules, CLI flags, REST endpoints and the `.omh` format) now follows semantic
|
|
27
|
+
versioning; default prompts and thresholds may still be tuned in minor releases.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Public, reproducible benchmark (`benchmark/`): 475 hand-reviewed, openly licensed
|
|
31
|
+
Wikimedia Commons photos across weapons, hate symbols, alcohol, tobacco, drugs, violence,
|
|
32
|
+
safe images and look-alikes, with a dev/test split. `python benchmark/run.py` reproduces
|
|
33
|
+
the published numbers.
|
|
34
|
+
- Documentation site (MkDocs) with guides, benchmarks and an API reference generated from
|
|
35
|
+
the docstrings, deployed to GitHub Pages.
|
|
36
|
+
- Release automation: pushing a `vX.Y.Z` tag builds the package, creates the GitHub
|
|
37
|
+
release from the changelog and (once enabled) publishes to PyPI.
|
|
38
|
+
- Type information (`py.typed`) and `mypy` in CI.
|
|
39
|
+
- Top-level exports: `LabelSpec`, `LinearHead`, `ImageLoadError`, `VideoLoadError`,
|
|
40
|
+
`VideoModerationResult`, `VideoSegment`, `SAFE_PROMPTS`.
|
|
41
|
+
- `SECURITY.md`, issue templates (including a misclassification report that asks people
|
|
42
|
+
not to attach harmful images) and a pull-request template.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- More complete default prompts, measured on the held-out test half: `drugs` recall 15% →
|
|
46
|
+
85%, `alcohol` 53% → 84%, `tobacco` 44% → 59%, `hate_symbols` 50% → 61%, with the safe
|
|
47
|
+
false-positive rate at 6% (was 5%). New safe look-alikes: milk, juice, coffee; kitchen
|
|
48
|
+
knives are now a `weapons` negative.
|
|
49
|
+
- `integrations.configure()` takes the moderator as a positional-only argument.
|
|
50
|
+
- The README is now an overview; detailed guides moved to the docs site.
|
|
51
|
+
|
|
52
|
+
## [0.5.0] - 2026-10-14
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- Video moderation (`pip install "openmoderate[video]"`): `Moderator.analyze_video()` samples
|
|
56
|
+
frames, scores them in batches and returns timestamped `segments` per violated label, with
|
|
57
|
+
`fps`, `min_frames`, `stop_on_violation`, `max_duration` and `keep_frame_scores` options.
|
|
58
|
+
- `openmoderate scan` handles video files (`--fps`, `--min-frames`, `--max-duration`).
|
|
59
|
+
- REST endpoint `POST /v1/moderate/video`, streamed to disk with size and duration limits
|
|
60
|
+
(`OPENMODERATE_MAX_VIDEO_BYTES`, `OPENMODERATE_MAX_VIDEO_SECONDS`). Docker image includes
|
|
61
|
+
video support.
|
|
62
|
+
- Decoding limits enforced while decoding (frame size, decoded frames, duration), not taken
|
|
63
|
+
from the file's metadata.
|
|
64
|
+
- Border trimming (`trim_borders`, on by default): letterbox bars and uniform padding are
|
|
65
|
+
cropped before scoring, for images, video frames, training and evaluation alike.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
- Border trimming changes some scores. On our test set it removed a false `sexual_activity`
|
|
69
|
+
detection on letterboxed video and turned a missed `alcohol` detection (a glass of wine on
|
|
70
|
+
a plain background, 0.54) into a hit (0.78). Set `trim_borders: false` to keep the old
|
|
71
|
+
behaviour.
|
|
72
|
+
|
|
73
|
+
## [0.4.0] - 2026-10-14
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
- Framework integrations under `openmoderate.integrations`:
|
|
77
|
+
- FastAPI: `ModeratedFile` / `ModeratedFiles` and `moderated()` / `moderated_files()`
|
|
78
|
+
dependencies, with moderation off the event loop.
|
|
79
|
+
- Flask: `@moderate_uploads(...)` decorator and an `OpenModerate` extension that can check
|
|
80
|
+
every request.
|
|
81
|
+
- Django: `ModerationValidator` (models, forms, admin, DRF; migration-safe) and
|
|
82
|
+
`ModerationMiddleware` for URL prefixes, configured via `settings.OPENMODERATE`.
|
|
83
|
+
- One shared model per process; per-endpoint policies reuse it instead of loading another copy.
|
|
84
|
+
- Uploads are rewound after checking so applications can save them normally.
|
|
85
|
+
- Error responses hide confidence scores by default (`include_scores=True` to show them).
|
|
86
|
+
- Extras: `openmoderate[fastapi]`, `[flask]`, `[django]`; runnable example apps for each.
|
|
87
|
+
- README infographic (`docs/assets/how-it-works.svg`) and animated terminal demo
|
|
88
|
+
(`docs/assets/demo.svg`), both following GitHub's light/dark theme. Regenerate with
|
|
89
|
+
`python scripts/make_readme_graphics.py`.
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
- The test suite's fake engine uses exactly orthogonal text vectors, making multi-label
|
|
93
|
+
assertions deterministic.
|
|
94
|
+
|
|
95
|
+
## [0.3.0] - 2026-10-14
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
- REST API server (`pip install "openmoderate[server]"`, `openmoderate serve`), built on FastAPI:
|
|
99
|
+
`GET /health`, `GET /v1/categories`, `POST /v1/moderate` (multipart), `POST /v1/moderate/batch`
|
|
100
|
+
(multipart, per-file errors) and `POST /v1/moderate/json` (base64 or URL).
|
|
101
|
+
- Optional API-key authentication via `OPENMODERATE_API_KEY` (constant-time comparison).
|
|
102
|
+
- URL fetching, off by default (`OPENMODERATE_ALLOW_URLS`), with SSRF protection: http(s) only,
|
|
103
|
+
no private, loopback or link-local addresses, every redirect re-validated, size and time limits.
|
|
104
|
+
- Upload size and batch limits, model preloading at startup, and a warning when serving on a public
|
|
105
|
+
interface without an API key.
|
|
106
|
+
- Dockerfile (CPU torch, non-root user, model weights baked in, healthcheck) and a CI job that
|
|
107
|
+
builds and smoke-tests the image.
|
|
108
|
+
- CI also runs on `release/**` branches, checks that the model stack imports, and reports
|
|
109
|
+
failures as annotations.
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
- Docker image and CI now install `torch` and `torchvision` from the same CPU index (a mismatch
|
|
113
|
+
broke `import open_clip`).
|
|
114
|
+
|
|
115
|
+
## [0.2.0] - 2026-10-14
|
|
116
|
+
|
|
117
|
+
### Added
|
|
118
|
+
- `openmoderate` command-line tool with `scan`, `train`, `evaluate`, `categories` and `download` commands.
|
|
119
|
+
- `scan` supports files, recursive folders and URLs, JSON-lines output, policy flags and `--fail-on-violation` for CI.
|
|
120
|
+
- Training on your own data: folder datasets (`dataset/<label>/`, `safe/`), a class-balanced linear head on frozen CLIP embeddings, held-out precision/recall/F1, then a refit on all data.
|
|
121
|
+
- `Moderator(head=..., head_weight=...)` adds trained labels and blends them with zero-shot categories.
|
|
122
|
+
- `.omh` head format (NumPy archive plus JSON metadata, no pickle), with a check that the head's model matches the moderator's.
|
|
123
|
+
- `Moderator.label_specs` and `Moderator.thresholds` properties.
|
|
124
|
+
- `train` warns when the `safe/` folder is too small to teach the head what normal traffic looks like.
|
|
125
|
+
|
|
126
|
+
## [0.1.0] - 2026-10-14
|
|
127
|
+
|
|
128
|
+
### Added
|
|
129
|
+
- `Moderator` with zero-shot image moderation powered by OpenCLIP (`ViT-B-32`, LAION-2B).
|
|
130
|
+
- 11 default categories: nudity, sexual_activity, suggestive, violence, gore, weapons, hate_symbols, drugs, self_harm, alcohol, tobacco.
|
|
131
|
+
- Calibrated per-category scores with configurable thresholds, plus category-specific `negatives` (benign look-alikes).
|
|
132
|
+
- Policy configuration from kwargs, dict or YAML, including custom categories.
|
|
133
|
+
- Image loading from paths, URLs, bytes, file objects and PIL images, with EXIF rotation, transparency flattening and size limits.
|
|
134
|
+
- `analyze_batch()` for efficient multi-image scoring.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use openmoderate in research or a product evaluation, please cite it as below."
|
|
3
|
+
title: "openmoderate: open-source, self-hosted image and video content moderation"
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- name: "openmoderate contributors"
|
|
7
|
+
repository-code: "https://github.com/karammm/openmoderate-image-moderation"
|
|
8
|
+
url: "https://karammm.github.io/openmoderate-image-moderation/"
|
|
9
|
+
license: Apache-2.0
|
|
10
|
+
version: 1.0.1
|
|
11
|
+
date-released: "2026-10-14"
|
|
12
|
+
keywords:
|
|
13
|
+
- content moderation
|
|
14
|
+
- trust and safety
|
|
15
|
+
- computer vision
|
|
16
|
+
- CLIP
|
|
17
|
+
- zero-shot classification
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Code of conduct
|
|
2
|
+
|
|
3
|
+
This project follows the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
|
4
|
+
|
|
5
|
+
In short: be welcoming and respectful, assume good intent, critique ideas rather than
|
|
6
|
+
people, and do not harass anyone. Because this project deals with harmful content, one
|
|
7
|
+
extra rule applies everywhere in this repository (issues, pull requests, discussions):
|
|
8
|
+
**never post or link harmful images** such as sexual content, gore, abuse or extremist
|
|
9
|
+
material. Describe them in words instead.
|
|
10
|
+
|
|
11
|
+
Report unacceptable behaviour privately to the maintainers through a
|
|
12
|
+
[GitHub security advisory](https://github.com/karammm/openmoderate-image-moderation/security/advisories/new)
|
|
13
|
+
(it is private and reaches the maintainers directly). Reports are handled confidentially,
|
|
14
|
+
and maintainers may remove content or ban participants who violate this code.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Contributing to openmoderate
|
|
2
|
+
|
|
3
|
+
Thanks for helping make open moderation better!
|
|
4
|
+
|
|
5
|
+
## Development setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/karammm/openmoderate-image-moderation.git
|
|
9
|
+
cd openmoderate-image-moderation
|
|
10
|
+
python -m venv .venv && source .venv/bin/activate # Python 3.9+
|
|
11
|
+
pip install -e ".[dev]"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Running checks
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
ruff check src tests # lint
|
|
18
|
+
ruff format src tests # format
|
|
19
|
+
pytest # fast unit tests (uses a fake engine, no model download)
|
|
20
|
+
pytest -m slow # smoke test against the real CLIP model (~600 MB download)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Guidelines
|
|
24
|
+
|
|
25
|
+
- Keep pull requests focused; add tests for behaviour changes.
|
|
26
|
+
- **Never commit explicit, violent or otherwise harmful images** to the repository. Unit tests use synthetic images and a fake engine. If you improve prompts or thresholds, describe your evaluation set in the PR (source, size, licence) instead of committing it.
|
|
27
|
+
- Prompt and threshold changes should include before/after numbers on the images you evaluated.
|
|
28
|
+
- Update `CHANGELOG.md` under "Unreleased".
|
|
29
|
+
|
|
30
|
+
## Releasing (maintainers)
|
|
31
|
+
|
|
32
|
+
1. Bump `__version__` in `src/openmoderate/__init__.py` and move the "Unreleased" notes in
|
|
33
|
+
`CHANGELOG.md` under a new `## [X.Y.Z] - YYYY-MM-DD` heading.
|
|
34
|
+
2. Merge to `main` once CI is green.
|
|
35
|
+
3. Tag and push: `git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z`.
|
|
36
|
+
|
|
37
|
+
The `Release` workflow then checks the tag matches `__version__`, builds the package,
|
|
38
|
+
creates the GitHub release (install instructions, the changelog section and links, with
|
|
39
|
+
the wheel and sdist attached), backfills release pages for older tags, and publishes to
|
|
40
|
+
PyPI. The `Docker image` workflow smoke-tests the server image and publishes it to
|
|
41
|
+
`ghcr.io/karammm/openmoderate` as `X.Y.Z`, `X.Y` and `latest`.
|
|
42
|
+
|
|
43
|
+
### One-time setup
|
|
44
|
+
|
|
45
|
+
**PyPI publishing** uses [trusted publishing](https://docs.pypi.org/trusted-publishers/):
|
|
46
|
+
the `openmoderate` project on PyPI trusts the `release.yml` workflow of this repository
|
|
47
|
+
(environment `pypi`), so no API token is stored anywhere. To move the project, update the
|
|
48
|
+
publisher under the project's *Settings → Publishing* on PyPI.
|
|
49
|
+
|
|
50
|
+
**Docs site:** in the GitHub repo, *Settings → Pages → Build and deployment → Source*,
|
|
51
|
+
choose **GitHub Actions**, then add the repository variable `DOCS_DEPLOY` = `true`. The
|
|
52
|
+
`Docs` workflow then deploys on every push to `main` (re-run it once to publish now).
|
|
53
|
+
|
|
54
|
+
**Social preview:** upload `docs/assets/social-preview.png` under *Settings → General →
|
|
55
|
+
Social preview* (GitHub has no API for this). Regenerate it with
|
|
56
|
+
`python scripts/make_readme_graphics.py` and a headless browser screenshot of the SVG.
|
|
57
|
+
|
|
58
|
+
## Benchmark
|
|
59
|
+
|
|
60
|
+
`python benchmark/run.py` measures accuracy on openly licensed Wikimedia Commons images
|
|
61
|
+
(see [`benchmark/README.md`](benchmark/README.md)). Run it before and after any change to
|
|
62
|
+
prompts, thresholds or scoring, and include both tables in your pull request.
|