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.
Files changed (88) hide show
  1. openmoderate-1.0.1/.dockerignore +13 -0
  2. openmoderate-1.0.1/.github/ISSUE_TEMPLATE/bug_report.yml +29 -0
  3. openmoderate-1.0.1/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. openmoderate-1.0.1/.github/ISSUE_TEMPLATE/misclassification.yml +39 -0
  5. openmoderate-1.0.1/.github/dependabot.yml +20 -0
  6. openmoderate-1.0.1/.github/pull_request_template.md +15 -0
  7. openmoderate-1.0.1/.github/workflows/ci.yml +66 -0
  8. openmoderate-1.0.1/.github/workflows/docker.yml +90 -0
  9. openmoderate-1.0.1/.github/workflows/docs.yml +41 -0
  10. openmoderate-1.0.1/.github/workflows/release.yml +84 -0
  11. openmoderate-1.0.1/.gitignore +17 -0
  12. openmoderate-1.0.1/CHANGELOG.md +134 -0
  13. openmoderate-1.0.1/CITATION.cff +17 -0
  14. openmoderate-1.0.1/CODE_OF_CONDUCT.md +14 -0
  15. openmoderate-1.0.1/CONTRIBUTING.md +62 -0
  16. openmoderate-1.0.1/Dockerfile +34 -0
  17. openmoderate-1.0.1/LICENSE +202 -0
  18. openmoderate-1.0.1/PKG-INFO +164 -0
  19. openmoderate-1.0.1/README.md +109 -0
  20. openmoderate-1.0.1/SECURITY.md +39 -0
  21. openmoderate-1.0.1/benchmark/README.md +77 -0
  22. openmoderate-1.0.1/benchmark/RESULTS.md +38 -0
  23. openmoderate-1.0.1/benchmark/manifest.csv +483 -0
  24. openmoderate-1.0.1/benchmark/results.json +394 -0
  25. openmoderate-1.0.1/benchmark/run.py +191 -0
  26. openmoderate-1.0.1/docs/assets/demo.svg +85 -0
  27. openmoderate-1.0.1/docs/assets/how-it-works.svg +149 -0
  28. openmoderate-1.0.1/docs/assets/social-preview.png +0 -0
  29. openmoderate-1.0.1/docs/assets/social-preview.svg +25 -0
  30. openmoderate-1.0.1/docs/benchmarks.md +56 -0
  31. openmoderate-1.0.1/docs/changelog.md +1 -0
  32. openmoderate-1.0.1/docs/guide/cli.md +12 -0
  33. openmoderate-1.0.1/docs/guide/concepts.md +46 -0
  34. openmoderate-1.0.1/docs/guide/integrations.md +82 -0
  35. openmoderate-1.0.1/docs/guide/python.md +63 -0
  36. openmoderate-1.0.1/docs/guide/server.md +66 -0
  37. openmoderate-1.0.1/docs/guide/training.md +45 -0
  38. openmoderate-1.0.1/docs/guide/video.md +36 -0
  39. openmoderate-1.0.1/docs/index.md +56 -0
  40. openmoderate-1.0.1/docs/reference.md +51 -0
  41. openmoderate-1.0.1/docs/requirements.txt +3 -0
  42. openmoderate-1.0.1/docs/security.md +1 -0
  43. openmoderate-1.0.1/examples/django_app.py +56 -0
  44. openmoderate-1.0.1/examples/fastapi_app.py +32 -0
  45. openmoderate-1.0.1/examples/flask_app.py +27 -0
  46. openmoderate-1.0.1/examples/policy.yaml +14 -0
  47. openmoderate-1.0.1/examples/quickstart.py +22 -0
  48. openmoderate-1.0.1/mkdocs.yml +64 -0
  49. openmoderate-1.0.1/pyproject.toml +75 -0
  50. openmoderate-1.0.1/scripts/changelog_section.py +29 -0
  51. openmoderate-1.0.1/scripts/make_readme_graphics.py +426 -0
  52. openmoderate-1.0.1/scripts/release_notes.py +76 -0
  53. openmoderate-1.0.1/src/openmoderate/__init__.py +43 -0
  54. openmoderate-1.0.1/src/openmoderate/categories.py +203 -0
  55. openmoderate-1.0.1/src/openmoderate/cli.py +390 -0
  56. openmoderate-1.0.1/src/openmoderate/config.py +100 -0
  57. openmoderate-1.0.1/src/openmoderate/engine/__init__.py +4 -0
  58. openmoderate-1.0.1/src/openmoderate/engine/base.py +19 -0
  59. openmoderate-1.0.1/src/openmoderate/engine/clip_engine.py +83 -0
  60. openmoderate-1.0.1/src/openmoderate/integrations/__init__.py +26 -0
  61. openmoderate-1.0.1/src/openmoderate/integrations/_common.py +176 -0
  62. openmoderate-1.0.1/src/openmoderate/integrations/django.py +189 -0
  63. openmoderate-1.0.1/src/openmoderate/integrations/fastapi.py +172 -0
  64. openmoderate-1.0.1/src/openmoderate/integrations/flask.py +149 -0
  65. openmoderate-1.0.1/src/openmoderate/io.py +115 -0
  66. openmoderate-1.0.1/src/openmoderate/moderator.py +274 -0
  67. openmoderate-1.0.1/src/openmoderate/py.typed +0 -0
  68. openmoderate-1.0.1/src/openmoderate/result.py +58 -0
  69. openmoderate-1.0.1/src/openmoderate/server/__init__.py +1 -0
  70. openmoderate-1.0.1/src/openmoderate/server/app.py +257 -0
  71. openmoderate-1.0.1/src/openmoderate/server/fetch.py +86 -0
  72. openmoderate-1.0.1/src/openmoderate/training/__init__.py +12 -0
  73. openmoderate-1.0.1/src/openmoderate/training/dataset.py +93 -0
  74. openmoderate-1.0.1/src/openmoderate/training/head.py +82 -0
  75. openmoderate-1.0.1/src/openmoderate/training/trainer.py +148 -0
  76. openmoderate-1.0.1/src/openmoderate/video.py +326 -0
  77. openmoderate-1.0.1/tests/conftest.py +142 -0
  78. openmoderate-1.0.1/tests/test_cli.py +58 -0
  79. openmoderate-1.0.1/tests/test_integration_django.py +89 -0
  80. openmoderate-1.0.1/tests/test_integration_fastapi.py +80 -0
  81. openmoderate-1.0.1/tests/test_integration_flask.py +85 -0
  82. openmoderate-1.0.1/tests/test_integrations_common.py +68 -0
  83. openmoderate-1.0.1/tests/test_moderator.py +138 -0
  84. openmoderate-1.0.1/tests/test_real_model.py +14 -0
  85. openmoderate-1.0.1/tests/test_scripts.py +95 -0
  86. openmoderate-1.0.1/tests/test_server.py +167 -0
  87. openmoderate-1.0.1/tests/test_training.py +91 -0
  88. openmoderate-1.0.1/tests/test_video.py +106 -0
@@ -0,0 +1,13 @@
1
+ .git
2
+ .github
3
+ .venv
4
+ venv
5
+ dist
6
+ build
7
+ **/__pycache__
8
+ **/*.pyc
9
+ .pytest_cache
10
+ .ruff_cache
11
+ tests
12
+ examples
13
+ *.omh
@@ -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,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Report a security vulnerability
4
+ url: https://github.com/karammm/openmoderate-image-moderation/security/advisories/new
5
+ about: Please report security problems privately, not as a public issue.
@@ -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,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .DS_Store
13
+ *.omh
14
+ .idea/
15
+ .vscode/
16
+ benchmark/data/
17
+ site/
@@ -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.