Merge pull request #5 from SlimusMinus/change-image_model

change image model
This commit is contained in:
SlimusMinus
2026-09-07 01:00:52 +03:00
committed by GitHub
6 changed files with 70 additions and 20 deletions

View File

@@ -3,8 +3,6 @@ FORBIDDEN_IMAGE_LABELS = {
s.lower() for s in { s.lower() for s in {
"a photo containing marijuana", "a photo containing marijuana",
"a photo containing drugs", "a photo containing drugs",
"a photo containing weapons",
"a pornographic photo",
"a photo containing violence", "a photo containing violence",
} }
} }

View File

@@ -21,6 +21,9 @@ class Settings(BaseSettings):
NSFW_THRESHOLD: float = 0.85 NSFW_THRESHOLD: float = 0.85
IMAGE_CLIP_MODEL: str = "openai/clip-vit-base-patch32" IMAGE_CLIP_MODEL: str = "openai/clip-vit-base-patch32"
CLIP_THRESHOLD: float = 0.75 CLIP_THRESHOLD: float = 0.75
WEAPON_MODEL: str = "google/owlv2-base-patch16-ensemble"
WEAPON_THRESHOLD: float = 0.40
WEAPON_MIN_AREA_FRACTION: float = 0.005
MODEL_CACHE_DIR: str = "./models" MODEL_CACHE_DIR: str = "./models"
DEVICE: str = "auto" DEVICE: str = "auto"

View File

@@ -2,6 +2,7 @@ from functools import lru_cache
from app.ml.image.clip_classifier import ClipClassifier from app.ml.image.clip_classifier import ClipClassifier
from app.ml.image.image_classifier import ImageClassifier from app.ml.image.image_classifier import ImageClassifier
from app.ml.image.weapon_detector import WeaponDetector
from app.ml.text_classifier import TextClassifier from app.ml.text_classifier import TextClassifier
from app.moderation.image.validator import ImageValidator from app.moderation.image.validator import ImageValidator
from app.moderation.profanity.detector import ProfanityDetector from app.moderation.profanity.detector import ProfanityDetector
@@ -49,10 +50,16 @@ def get_image_classifier() -> ImageClassifier:
return ImageClassifier() return ImageClassifier()
@lru_cache
def get_weapon_detector() -> WeaponDetector:
return WeaponDetector()
@lru_cache @lru_cache
def get_image_moderation_service() -> ImageModerationService: def get_image_moderation_service() -> ImageModerationService:
return ImageModerationService( return ImageModerationService(
classifier=get_image_classifier(), classifier=get_image_classifier(),
validator=get_image_validator(), validator=get_image_validator(),
clip_classifier=get_clip_classifier() clip_classifier=get_clip_classifier(),
weapon_detector=get_weapon_detector()
) )

View File

@@ -11,8 +11,6 @@ class ClipClassifier:
"a normal photo", "a normal photo",
"a photo containing marijuana", "a photo containing marijuana",
"a photo containing drugs", "a photo containing drugs",
"a photo containing weapons",
"a pornographic photo",
"a photo containing violence" "a photo containing violence"
] ]

View File

@@ -5,6 +5,7 @@ from app.config.settings import settings
from app.ml.image.clip_classifier import ClipClassifier from app.ml.image.clip_classifier import ClipClassifier
from app.ml.image.image_classifier import ImageClassifier from app.ml.image.image_classifier import ImageClassifier
from app.ml.image.image_prediction_result import ImagePredictionResult from app.ml.image.image_prediction_result import ImagePredictionResult
from app.ml.image.weapon_detector import WeaponDetector
from app.moderation.image.validator import ImageValidator from app.moderation.image.validator import ImageValidator
class ImageModerationService: class ImageModerationService:
@@ -13,11 +14,13 @@ class ImageModerationService:
self, self,
classifier: ImageClassifier, classifier: ImageClassifier,
validator: ImageValidator, validator: ImageValidator,
clip_classifier: ClipClassifier clip_classifier: ClipClassifier,
weapon_detector: WeaponDetector
): ):
self.classifier = classifier self.classifier = classifier
self.clip_classifier = clip_classifier self.clip_classifier = clip_classifier
self.weapon_detector = weapon_detector
self.validator = validator self.validator = validator
def moderate( def moderate(
@@ -48,7 +51,30 @@ class ImageModerationService:
return nsfw_prediction return nsfw_prediction
# ========================= # =========================
# 2. CLIP MODEL # 2. WEAPON DETECTOR
# =========================
weapon_prediction = (
self.weapon_detector.predict(image)
)
if weapon_prediction.detected_labels:
return ImagePredictionResult(
label=weapon_prediction.label,
score=weapon_prediction.score,
approved=False,
reason="FORBIDDEN_CONTENT",
detected_labels=weapon_prediction.detected_labels
)
# =========================
# 3. CLIP MODEL
# ========================= # =========================
clip_prediction = ( clip_prediction = (
@@ -82,7 +108,7 @@ class ImageModerationService:
) )
# ========================= # =========================
# 3. NORMAL IMAGE # 4. NORMAL IMAGE
# ========================= # =========================
return ImagePredictionResult( return ImagePredictionResult(

View File

@@ -3,7 +3,7 @@
Сервис автоматической модерации пользовательского контента на базе **FastAPI** и моделей машинного обучения. Позволяет проверять тексты и изображения на наличие недопустимого контента: Сервис автоматической модерации пользовательского контента на базе **FastAPI** и моделей машинного обучения. Позволяет проверять тексты и изображения на наличие недопустимого контента:
- **Тексты** — токсичность (нейросетевая модель BERT) и нецензурная лексика (словарь + морфологический анализ `pymorphy3`). - **Тексты** — токсичность (нейросетевая модель BERT) и нецензурная лексика (словарь + морфологический анализ `pymorphy3`).
- **Изображения** — NSFW-контент (нейросетевая модель `Falconsai/nsfw_image_detection`) и запрещённые категории (марихуана, наркотики, оружие, порнография, насилие) через мультимодальную модель CLIP. - **Изображения** — NSFW-контент (нейросетевая модель `Falconsai/nsfw_image_detection`), оружие (zero-shot детектор **OWLv2**) и запрещённые категории (марихуана, наркотики, порнография, насилие) через мультимодальную модель CLIP.
Сервис предоставляет REST API и возвращает вердикт (`approved`), оценку уверенности (`score`), причину отказа (`reason`) и дополнительную диагностическую информацию. Сервис предоставляет REST API и возвращает вердикт (`approved`), оценку уверенности (`score`), причину отказа (`reason`) и дополнительную диагностическую информацию.
@@ -39,7 +39,8 @@
- **Модерация изображений**: - **Модерация изображений**:
- Проверка формата (JPEG/PNG/WebP) и размера файла (до 5 МБ); - Проверка формата (JPEG/PNG/WebP) и размера файла (до 5 МБ);
- Валидация геометрии изображения (50×50 … 10000×10000 пикселей); - Валидация геометрии изображения (50×50 … 10000×10000 пикселей);
- Двухэтапная проверка: модель NSFW-классификации + CLIP-классификация по заранее заданным категориям; - Комбинированная проверка: модель NSFW-классификации + CLIP-классификация по запрещённым категориям (наркотики, насилие);
- Отдельный детектор оружия на базе **OWLv2** (zero-shot object detection) — замена ненадёжной CLIP-проверке категории `weapons`;
- Комбинированный вердикт на основе нескольких моделей. - Комбинированный вердикт на основе нескольких моделей.
- **Готовый REST API** с автоматической документацией **Swagger UI** (`/docs`) и **ReDoc** (`/redoc`). - **Готовый REST API** с автоматической документацией **Swagger UI** (`/docs`) и **ReDoc** (`/redoc`).
- **Автоматическая загрузка ML-моделей** при старте приложения (кэшируются локально). - **Автоматическая загрузка ML-моделей** при старте приложения (кэшируются локально).
@@ -57,6 +58,7 @@
| Работа с переменными окружения | python-dotenv | | Работа с переменными окружения | python-dotenv |
| Нейросетевые модели | Transformers (Hugging Face), PyTorch | | Нейросетевые модели | Transformers (Hugging Face), PyTorch |
| Мультимодальная модель | OpenAI CLIP (ViT-B/32) | | Мультимодальная модель | OpenAI CLIP (ViT-B/32) |
| Детектор оружия | OWLv2 (`google/owlv2-base-patch16-ensemble`) |
| Морфология русского языка | pymorphy3 | | Морфология русского языка | pymorphy3 |
| Обработка изображений | Pillow (PIL) | | Обработка изображений | Pillow (PIL) |
| Язык | Python 3.12 | | Язык | Python 3.12 |
@@ -100,7 +102,9 @@ moderation-post/
│ │ ├── image_classifier.py # Классификатор NSFW изображений │ │ ├── image_classifier.py # Классификатор NSFW изображений
│ │ ├── image_prediction_result.py # Результат классификации изображений │ │ ├── image_prediction_result.py # Результат классификации изображений
│ │ ├── clip_classifier.py # CLIP-классификатор по категориям │ │ ├── clip_classifier.py # CLIP-классификатор по категориям
│ │ └── clip_prediction_result.py # Результат CLIP-классификации │ │ ├── clip_prediction_result.py # Результат CLIP-классификации
│ │ ├── weapon_detector.py # Детектор оружия (OWLv2)
│ │ └── weapon_prediction_result.py # Результат детекции оружия
│ ├── models/ │ ├── models/
│ │ ├── dto/ │ │ ├── dto/
│ │ │ └── text_request.py # DTO запроса модерации текста │ │ │ └── text_request.py # DTO запроса модерации текста
@@ -183,6 +187,7 @@ DEBUG=true
TEXT_MODEL=textdetox/bert-multilingual-toxicity-classifier TEXT_MODEL=textdetox/bert-multilingual-toxicity-classifier
IMAGE_MODEL=Falconsai/nsfw_image_detection IMAGE_MODEL=Falconsai/nsfw_image_detection
IMAGE_CLIP_MODEL=openai/clip-vit-base-patch32 IMAGE_CLIP_MODEL=openai/clip-vit-base-patch32
WEAPON_MODEL=google/owlv2-base-patch16-ensemble
MODEL_CACHE_DIR=./models MODEL_CACHE_DIR=./models
DEVICE=auto DEVICE=auto
HF_TOKEN=<ваш HF-токен> HF_TOKEN=<ваш HF-токен>
@@ -230,6 +235,9 @@ fastapi run app/main.py --host 0.0.0.0 --port 8000
| `NSFW_THRESHOLD` | float | `0.85` | Порог уверенности NSFW-модели | | `NSFW_THRESHOLD` | float | `0.85` | Порог уверенности NSFW-модели |
| `IMAGE_CLIP_MODEL` | str | `openai/clip-vit-base-patch32` | Мультимодальная модель CLIP | | `IMAGE_CLIP_MODEL` | str | `openai/clip-vit-base-patch32` | Мультимодальная модель CLIP |
| `CLIP_THRESHOLD` | float | `0.75` | Порог уверенности CLIP-классификатора | | `CLIP_THRESHOLD` | float | `0.75` | Порог уверенности CLIP-классификатора |
| `WEAPON_MODEL` | str | `google/owlv2-base-patch16-ensemble` | Модель zero-shot детекции оружия (OWLv2) |
| `WEAPON_THRESHOLD` | float | `0.40` | Порог уверенности детектора оружия |
| `WEAPON_MIN_AREA_FRACTION` | float | `0.005` | Минимальная доля площади изображения для бокса (0.5%), отсекает мелкие ложные срабатывания |
| `HF_TOKEN` | str | — | Токен Hugging Face (подписанный/приватные модели) | | `HF_TOKEN` | str | — | Токен Hugging Face (подписанный/приватные модели) |
| `MODEL_CACHE_DIR` | str | `./models` | Каталог локального кэша моделей | | `MODEL_CACHE_DIR` | str | `./models` | Каталог локального кэша моделей |
| `DEVICE` | str | `auto` | Устройство инференса: `auto`, `cpu`, `cuda` и т.д. | | `DEVICE` | str | `auto` | Устройство инференса: `auto`, `cpu`, `cuda` и т.д. |
@@ -447,10 +455,10 @@ class ClipPredictionResult:
- **`image_policy.py`** — множество `FORBIDDEN_IMAGE_LABELS` с запрещёнными категориями CLIP: - **`image_policy.py`** — множество `FORBIDDEN_IMAGE_LABELS` с запрещёнными категориями CLIP:
- `a photo containing marijuana` - `a photo containing marijuana`
- `a photo containing drugs` - `a photo containing drugs`
- `a photo containing weapons`
- `a pornographic photo`
- `a photo containing violence` - `a photo containing violence`
Порнография и оружие в этот список не входят: первая обрабатывается NSFW-моделью (шаг 1), второе — отдельным детектором на базе OWLv2 (шаг 2).
### DI-фабрики — `app/core/dependencies.py` ### DI-фабрики — `app/core/dependencies.py`
Использует `@lru_cache`, что превращает фабрики в синглтоны: Использует `@lru_cache`, что превращает фабрики в синглтоны:
@@ -460,9 +468,10 @@ class ClipPredictionResult:
| `get_text_classifier` | `TextClassifier` | | `get_text_classifier` | `TextClassifier` |
| `get_clip_classifier` | `ClipClassifier` | | `get_clip_classifier` | `ClipClassifier` |
| `get_image_classifier` | `ImageClassifier` | | `get_image_classifier` | `ImageClassifier` |
| `get_weapon_detector` | `WeaponDetector` (OWLv2) |
| `get_profanity_detector` | `ProfanityDetector` (словарь + лемматизатор) | | `get_profanity_detector` | `ProfanityDetector` (словарь + лемматизатор) |
| `get_text_moderation_service` | `TextModerationService` | | `get_text_moderation_service` | `TextModerationService` |
| `get_image_moderation_service` | `ImageModerationService` (классификатор + валидатор + CLIP) | | `get_image_moderation_service` | `ImageModerationService` (классификатор + валидатор + CLIP + детектор оружия) |
| `get_image_validator` | `ImageValidator` | | `get_image_validator` | `ImageValidator` |
> Файл `app/container.py` содержит аналогичный, но неиспользуемый «ручной» контейнер, создающий детектор нецензурной лексики. Основной путь внедрения зависимостей — через `core/dependencies.py`. > Файл `app/container.py` содержит аналогичный, но неиспользуемый «ручной» контейнер, создающий детектор нецензурной лексики. Основной путь внедрения зависимостей — через `core/dependencies.py`.
@@ -485,9 +494,15 @@ class ClipPredictionResult:
- возвращает `ImagePredictionResult` с меткой из `id2label` (обычно `nsfw`/`normal`); - возвращает `ImagePredictionResult` с меткой из `id2label` (обычно `nsfw`/`normal`);
- **`image/clip_classifier.py`** — `ClipClassifier`: - **`image/clip_classifier.py`** — `ClipClassifier`:
- загружает CLIP-модель и процессор сразу в конструкторе (при первом обращении); - загружает CLIP-модель и процессор сразу в конструкторе (при первом обращении);
- задаёт список из 6 текстовых меток (`a normal photo` + 5 запрещённых); - задаёт список из 4 текстовых меток (`a normal photo` + 3 запрещённые: наркотики, насилие). Порнография отдельно обрабатывается NSFW-моделью на шаге 1;
- считает вероятности соответствия изображения каждой метке; - считает вероятности соответствия изображения каждой метке;
- собирает метки, превышающие `CLIP_THRESHOLD`, и возвращает `ClipPredictionResult`. - собирает метки, превышающие `CLIP_THRESHOLD`, и возвращает `ClipPredictionResult`;
- **`image/weapon_detector.py`** — `WeaponDetector`:
- загружает OWLv2-модель и процессор (zero-shot object detection) при первом обращении;
- ищет по 15 текстовым запросам: `gun`, `pistol`, `rifle`, `shotgun`, `knife`, `machete`, `sword`, `bomb`, `grenade` и др.;
- через `post_process_object_detection` получает боксы и уверенность по каждой запрошенной категории;
- отсеивает боксы меньше `WEAPON_MIN_AREA_FRACTION` от площади изображения и с уверенностью ниже `WEAPON_THRESHOLD`;
- возвращает `WeaponPredictionResult` с найденными категориями.
### Слой правил — `app/moderation/` ### Слой правил — `app/moderation/`
@@ -531,12 +546,15 @@ class ClipPredictionResult:
1. **Валидация.** `ImageValidator.validate(image)` — проверка размеров (см. выше). 1. **Валидация.** `ImageValidator.validate(image)` — проверка размеров (см. выше).
2. **Шаг 1 — NSFW-модель.** `ImageClassifier.predict(image)`: 2. **Шаг 1 — NSFW-модель.** `ImageClassifier.predict(image)`:
- если метка `nsfw` и уверенность ≥ `NSFW_THRESHOLD` (0.85) → решение **отклонить**: `reason="NSFW"`, `detected_labels=["nsfw"]`; - если метка `nsfw` и уверенность ≥ `NSFW_THRESHOLD` (0.85) → решение **отклонить**: `reason="NSFW"`, `detected_labels=["nsfw"]`;
3. **Шаг 2 — CLIP-модель.** `ClipClassifier.predict(image)`: 3. **Шаг 2 — детектор оружия.** `WeaponDetector.predict(image)`:
- из меток, превысивших `CLIP_THRESHOLD`, отбираются только те, что входят в `FORBIDDEN_IMAGE_LABELS`; - zero-shot детекция по запросам оружия (OWLv2, боксы + уверенность);
- если найдена категория с уверенностью ≥ `WEAPON_THRESHOLD` и боксом больше `WEAPON_MIN_AREA_FRACTION` → решение **отклонить**: `reason="FORBIDDEN_CONTENT"`, `detected_labels=[...]`;
4. **Шаг 3 — CLIP-модель.** `ClipClassifier.predict(image)`:
- из меток, превысивших `CLIP_THRESHOLD`, отбираются только те, что входят в `FORBIDDEN_IMAGE_LABELS` (наркотики, насилие);
- если такие категории найдены → решение **отклонить**: `reason="FORBIDDEN_CONTENT"`, `detected_labels=[...]`; - если такие категории найдены → решение **отклонить**: `reason="FORBIDDEN_CONTENT"`, `detected_labels=[...]`;
4. **Шаг 3 — «нормальное» изображение.** Если ни одно из правил не сработало → `approved=True`, `reason="OK"`, `label="normal"`, `score=1.0`. 5. **Шаг 4 — «нормальное» изображение.** Если ни одно из правил не сработало → `approved=True`, `reason="OK"`, `label="normal"`, `score=1.0`.
Таким образом, изображение одобряется только если оно прошло и NSFW-модель, и CLIP-проверку по запрещённым категориям. Таким образом, изображение одобряется только если оно прошло и NSFW-модель, и детектор оружия, и CLIP-проверку по запрещённым категориям.
--- ---