2026-09-07 01:00:52 +03:00
2026-07-17 01:06:36 +03:00
2026-09-07 00:59:22 +03:00
2026-08-06 23:45:38 +03:00
2026-07-17 01:06:36 +03:00
2026-09-07 00:59:22 +03:00

AI Moderation Service

Сервис автоматической модерации пользовательского контента на базе FastAPI и моделей машинного обучения. Позволяет проверять тексты и изображения на наличие недопустимого контента:

  • Тексты — токсичность (нейросетевая модель BERT) и нецензурная лексика (словарь + морфологический анализ pymorphy3).
  • Изображения — NSFW-контент (нейросетевая модель Falconsai/nsfw_image_detection), оружие (zero-shot детектор OWLv2) и запрещённые категории (марихуана, наркотики, порнография, насилие) через мультимодальную модель CLIP.

Сервис предоставляет REST API и возвращает вердикт (approved), оценку уверенности (score), причину отказа (reason) и дополнительную диагностическую информацию.


Оглавление


Возможности

  • Модерация текста:
    • Проверка на нецензурную лексику по словарю из ~7 350 слов (включая транслит и цифро-замены вроде «6ля»);
    • Лемматизация русских слов через pymorphy3 для поиска всех словоформ;
    • Нейросетевая оценка токсичности мультиязычной моделью textdetox/bert-multilingual-toxicity-classifier;
    • Настраиваемый порог токсичности (по умолчанию 0.90).
  • Модерация изображений:
    • Проверка формата (JPEG/PNG/WebP) и размера файла (до 5 МБ);
    • Валидация геометрии изображения (50×50 … 10000×10000 пикселей);
    • Комбинированная проверка: модель NSFW-классификации + CLIP-классификация по запрещённым категориям (наркотики, насилие);
    • Отдельный детектор оружия на базе OWLv2 (zero-shot object detection) — замена ненадёжной CLIP-проверке категории weapons;
    • Комбинированный вердикт на основе нескольких моделей.
  • Готовый REST API с автоматической документацией Swagger UI (/docs) и ReDoc (/redoc).
  • Автоматическая загрузка ML-моделей при старте приложения (кэшируются локально).
  • Автовыбор устройства выполнения инференса: CUDA, если доступен, иначе CPU.

Технологический стек

Компонент Технология
Веб-фреймворк FastAPI 0.139
ASGI-сервер Uvicorn 0.51
Валидация данных Pydantic 2.13, Pydantic-Settings
Работа с переменными окружения python-dotenv
Нейросетевые модели Transformers (Hugging Face), PyTorch
Мультимодальная модель OpenAI CLIP (ViT-B/32)
Детектор оружия OWLv2 (google/owlv2-base-patch16-ensemble)
Морфология русского языка pymorphy3
Обработка изображений Pillow (PIL)
Язык Python 3.12

Примечание. В файле ai-moderation/requirements.txt перечислены только базовые зависимости веб-фреймворка. Зависимости машинного обучения (transformers, torch, Pillow, pymorphy3 и др.) в этом файле отсутствуют и должны быть установлены дополнительно — см. раздел Установка и запуск.


Структура проекта

moderation-post/
├── .env                              # Конфигурация приложения (переменные окружения)
├── moderation-post.iml               # Файл модуля PyCharm (IDEA)
├── .idea/                            # Настройки IDE
├── readme.md                         # Данный документ
└── ai-moderation/                    # Корень Python-приложения
    ├── requirements.txt              # Базовые зависимости (без ML-пакетов)
    ├── models/                       # Локальный кэш моделей Hugging Face
    ├── app/
    │   ├── main.py                   # Точка входа: создание FastAPI-приложения
    │   ├── container.py              # Ручной DI-контейнер (словарь + детектор)
    │   ├── lifespan.py               # Жизненный цикл: загрузка моделей при старте
    │   ├── config/
    │   │   ├── settings.py           # Настройки (pydantic-settings), чтение .env
    │   │   ├── logging.py            # Настройка логирования
    │   │   └── image_policy.py       # Список запрещённых категорий для изображений
    │   ├── core/
    │   │   └── dependencies.py       # DI-фабрики (lru_cache) для сервисов/классификаторов
    │   ├── exceptions/
    │   │   ├── moderation_exception.py      # Базовое исключение модерации
    │   │   ├── invalid_text_exception.py    # Ошибка «некорректный текст»
    │   │   ├── invalid_image_exception.py   # Ошибка «некорректное изображение»
    │   │   └── handlers.py                  # Обработчики ошибок → JSON
    │   ├── ml/
    │   │   ├── base_classifier.py           # Абстрактный базовый классификатор
    │   │   ├── model_manager.py             # Синглтон загрузки и хранения моделей
    │   │   ├── prediction_result.py         # Результат классификации текста
    │   │   ├── text_classifier.py           # Классификатор токсичности текста
    │   │   └── image/
    │   │       ├── image_classifier.py           # Классификатор NSFW изображений
    │   │       ├── image_prediction_result.py    # Результат классификации изображений
    │   │       ├── clip_classifier.py            # CLIP-классификатор по категориям
    │   │       ├── clip_prediction_result.py     # Результат CLIP-классификации
    │   │       ├── weapon_detector.py            # Детектор оружия (OWLv2)
    │   │       └── weapon_prediction_result.py   # Результат детекции оружия
    │   ├── models/
    │   │   ├── dto/
    │   │   │   └── text_request.py      # DTO запроса модерации текста
    │   │   └── response/
    │   │       ├── moderation_response.py        # Ответ для текста
    │   │       ├── image_moderation_response.py  # Ответ для изображения
    │   │       └── error_response.py             # Универсальный ответ об ошибке
    │   ├── moderation/
    │   │   ├── image/
    │   │   │   └── validator.py        # Проверка размеров изображения
    │   │   └── profanity/
    │   │       ├── dictionary.py       # Загрузка словаря нецензурной лексики
    │   │       ├── lemmatizer.py       # Лемматизация через pymorphy3
    │   │       └── detector.py         # Детектор нецензурной лексики в тексте
    │   ├── resources/
    │   │   └── profanity_words.txt     # Словарь нецензурных слов (~7 350 записей)
    │   ├── routers/
    │   │   ├── text_moderation_router.py   # Эндпоинт POST /api/v1/moderation/text
    │   │   └── image_moderation_router.py  # Эндпоинт POST /api/v1/moderation/image
    │   └── services/
    │       ├── text_moderation_service.py  # Бизнес-логика модерации текста
    │       └── image_moderation_service.py # Бизнес-логика модерации изображений
    └── venv/                          # Виртуальное окружение Python

Установка и запуск

1. Клонирование репозитория

git clone <url-репозитория>
cd moderation-post

2. Создание виртуального окружения

cd ai-moderation
python -m venv venv

# Активация (Windows)
venv\Scripts\activate
# или (Linux/macOS)
source venv/bin/activate

3. Установка зависимостей

Файл requirements.txt содержит только веб-зависимости. Поскольку в requirements.txt не входят ML-пакеты, установите зависимости полностью:

pip install -r requirements.txt

Дополнительно (необходимо для работы ML-части, но не перечислено в requirements.txt):

pip install torch
pip install transformers
pip install Pillow
pip install pymorphy3

Важно. Для работы на GPU установите torch с соответствующим индексом CUDA (см. официальную документацию PyTorch), либо оставьте CPU-версию — сервис автоматически выберет доступное устройство.

4. Настройка окружения

Создайте файл .env в корне проекта (рядом с каталогом ai-moderation) или в самом каталоге приложения. Минимальный рабочий конфиг:

APP_NAME=AI Moderation Service
APP_VERSION=1.0.0
HOST=0.0.0.0
PORT=8000
DEBUG=true

# ===== AI =====
TEXT_MODEL=textdetox/bert-multilingual-toxicity-classifier
IMAGE_MODEL=Falconsai/nsfw_image_detection
IMAGE_CLIP_MODEL=openai/clip-vit-base-patch32
WEAPON_MODEL=google/owlv2-base-patch16-ensemble
MODEL_CACHE_DIR=./models
DEVICE=auto
HF_TOKEN=<ваш HF-токен>

Внимание: в файле .env и в settings.py по умолчанию уже вписаны значения HF_TOKEN, в том числе реальный токен. Рекомендуется не публиковать его и заменить на собственный. Публиковать секреты в репозиторий недопустимо.

5. Запуск сервера

Из каталога ai-moderation:

python -m uvicorn app.main:app --host 0.0.0.0 --port 8000

или через FastAPI CLI:

fastapi run app/main.py --host 0.0.0.0 --port 8000

При старте сервис скачает указанные модели в каталог MODEL_CACHE_DIR (если их ещё нет) и загрузит их в память. На машине без GPU первая загрузка может занять несколько минут.

6. Проверка работоспособности


Конфигурация

Настройки определяются классом Settings в app/config/settings.py (pydantic-settings) и читаются из файла .env. Все параметры имеют значения по умолчанию.

Переменная Тип По умолчанию Описание
APP_NAME str AI Moderation Название сервиса (отображается в Swagger)
APP_VERSION str 1.0.0 Версия сервиса
HOST str 0.0.0.0 Хост для запуска Uvicorn
PORT int 8000 Порт для запуска Uvicorn
DEBUG bool True Режим отладки
TEXT_TOXIC_THRESHOLD float 0.90 Порог токсичности текста: если вероятность «токсично» ≥ порога — текст отклоняется
TEXT_MODEL str textdetox/bert-multilingual-toxicity-classifier Модель классификации токсичности текста
IMAGE_MODEL str Falconsai/nsfw_image_detection Модель NSFW-классификации изображений
NSFW_THRESHOLD float 0.85 Порог уверенности NSFW-модели
IMAGE_CLIP_MODEL str openai/clip-vit-base-patch32 Мультимодальная модель 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 (подписанный/приватные модели)
MODEL_CACHE_DIR str ./models Каталог локального кэша моделей
DEVICE str auto Устройство инференса: auto, cpu, cuda и т.д.

API

Все эндпоинты находятся под префиксом /api/v1/moderation. Сервис не содержит авторизации — эндпоинты открыты.

POST /api/v1/moderation/text

Модерация текста. Запрос принимает JSON.

Тело запроса (модель TextRequest):

{
  "text": "Пример проверяемого текста"
}
Поле Тип Ограничения
text string обязательное, длина от 1 до 5000 символов

Успешный ответ — 200 OK (модель ModerationResponse):

{
  "approved": true,
  "score": 0.015,
  "reason": "OK"
}
Поле Тип Описание
approved boolean true — контент разрешён, false — отклонён
score float Уровень токсичности (0.0 … 1.0)
reason string Причина решения: OK, TOXIC, PROFANITY

Коды ошибок:

  • 400 — пустой текст / невалидная длина (INVALID_TEXT).

POST /api/v1/moderation/image

Модерация изображения. Запрос — multipart/form-data с полем file (тип UploadFile).

Ограничения загрузки:

  • Допустимые MIME-типы: image/jpeg, image/png, image/webp;
  • Максимальный размер файла: 5 МБ;
  • Допустимые размеры изображения: ширина и высота от 50 до 10000 пикселей.

Успешный ответ — 200 OK (модель ImageModerationResponse):

{
  "approved": true,
  "score": 1.0,
  "reason": "OK",
  "label": "normal",
  "detected_labels": []
}

Пример ответа при отклонении:

{
  "approved": false,
  "score": 0.93,
  "reason": "NSFW",
  "label": "nsfw",
  "detected_labels": ["nsfw"]
}
Поле Тип Описание
approved boolean Разрешено ли изображение
score float Уверенность модели, на которой принято решение
reason string Причина: OK, NSFW, FORBIDDEN_CONTENT
label string Наиболее вероятная метка (например, normal, nsfw или метка CLIP)
detected_labels array of strings Список обнаруженных запрещённых категорий

Коды ошибок:

  • 400 — недопустимый формат (Unsupported image format), битый файл (Invalid image), слишком маленькое/большое изображение (INVALID_IMAGE);
  • 413 — файл больше 5 МБ (Image too large).

Модели данных

Запросы (DTO)

TextRequest (app/models/dto/text_request.py)

class TextRequest(BaseModel):
    text: str = Field(min_length=1, max_length=5000)

Ответы

ModerationResponse (app/models/response/moderation_response.py) — ответ для текста:

class ModerationResponse(BaseModel):
    approved: bool
    score: float
    reason: str

ImageModerationResponse (app/models/response/image_moderation_response.py) — ответ для изображения:

class ImageModerationResponse(BaseModel):
    approved: bool
    score: float
    reason: str
    label: str
    detected_labels: List[str]

ErrorResponse (app/models/response/error_response.py) — универсальный ответ об ошибке:

class ErrorResponse(BaseModel):
    code: str
    message: str

Внутренние структуры (ML)

PredictionResult (app/ml/prediction_result.py) — результат классификации текста:

@dataclass(slots=True)
class PredictionResult:
    label: str
    score: float
    approved: bool
    raw_scores: list[float]
    reason: str = ""
    detected_words: list[str] = field(default_factory=list)

ImagePredictionResult (app/ml/image/image_prediction_result.py) — результат классификации изображения:

@dataclass(slots=True)
class ImagePredictionResult:
    label: str
    score: float
    approved: bool
    raw_scores: list[float] = field(default_factory=list)
    reason: str = ""
    detected_labels: list[str] = field(default_factory=list)

ClipPredictionResult (app/ml/image/clip_prediction_result.py) — результат CLIP-классификации:

@dataclass(slots=True)
class ClipPredictionResult:
    label: str            # метка с максимальной вероятностью
    score: float          # вероятность метки
    detected_labels: list[str]  # метки выше порога CLIP_THRESHOLD
    scores: dict[str, float]    # все метки → вероятности

Архитектура и слои

Проект построен по классической слоистой схеме:

Роутеры (routers)
    ↓
Сервисы (services) — бизнес-логика модерации
    ↓
Классификаторы (ml) + Правила (moderation)
    ↓
Модели Hugging Face / словарь нецензурной лексики

Зависимости компонентов внедряются через фабрики с декоратором @lru_cache (app/core/dependencies.py), поэтому в рамках одного процесса создаётся по одному экземпляру каждого классификатора и сервиса. Загрузка моделей выполняется один раз в lifespan при старте приложения.

Точка входа — app/main.py

  • Создаёт экземпляр FastAPI с названием и версией из настроек;
  • Подключает роутеры модерации текста и изображений;
  • Регистрирует глобальные обработчики исключений InvalidTextException и InvalidImageException;
  • Жизненный цикл приложения задан в lifespan.

Жизненный цикл — app/lifespan.py

При старте:

  1. Если задан HF_TOKEN, он экспортируется в переменную окружения HF_TOKEN;
  2. Вызывается model_manager.initialize() — загрузка устройства, токенизатора, текстовой и графической моделей.

При остановке сервис просто логирует завершение работы (освобождение GPU-памяти реализовано на уровне процесса/ОС).

Конфигурация — app/config/

  • settings.py — класс Settings (pydantic-settings) + экземпляр settings. Загружает .env, игнорирует лишние ключи (extra="ignore").

  • logging.py — конфигурация логирования в stdout (logging.INFO), логгер ai-moderation.

  • image_policy.py — множество FORBIDDEN_IMAGE_LABELS с запрещёнными категориями CLIP:

    • a photo containing marijuana
    • a photo containing drugs
    • a photo containing violence

    Порнография и оружие в этот список не входят: первая обрабатывается NSFW-моделью (шаг 1), второе — отдельным детектором на базе OWLv2 (шаг 2).

DI-фабрики — app/core/dependencies.py

Использует @lru_cache, что превращает фабрики в синглтоны:

Фабрика Возвращает
get_text_classifier TextClassifier
get_clip_classifier ClipClassifier
get_image_classifier ImageClassifier
get_weapon_detector WeaponDetector (OWLv2)
get_profanity_detector ProfanityDetector (словарь + лемматизатор)
get_text_moderation_service TextModerationService
get_image_moderation_service ImageModerationService (классификатор + валидатор + CLIP + детектор оружия)
get_image_validator ImageValidator

Файл app/container.py содержит аналогичный, но неиспользуемый «ручной» контейнер, создающий детектор нецензурной лексики. Основной путь внедрения зависимостей — через core/dependencies.py.

Слой ML — app/ml/

  • base_classifier.py — абстрактный класс BaseClassifier с методом predict(value);
  • model_manager.py — синглтон model_manager, который:
    • выбирает устройство (auto → CUDA при доступности, иначе CPU);
    • загружает токенизатор и текстовую модель (AutoTokenizer, AutoModelForSequenceClassification);
    • загружает процессор и модель изображений (AutoImageProcessor, AutoModelForImageClassification);
    • переводит модели в режим eval();
  • text_classifier.py — TextClassifier.predict(text):
    • токенизирует текст (макс. 512 токенов, truncation, padding);
    • прогоняет через модель без градиентов (torch.no_grad);
    • применяет softmax к логитам;
    • возвращает PredictionResult со score = вероятность класса 1 (токсичный);
  • image/image_classifier.py — ImageClassifier.predict(image):
    • обрабатывает изображение процессором модели;
    • возвращает ImagePredictionResult с меткой из id2label (обычно nsfw/normal);
  • image/clip_classifier.py — ClipClassifier:
    • загружает CLIP-модель и процессор сразу в конструкторе (при первом обращении);
    • задаёт список из 4 текстовых меток (a normal photo + 3 запрещённые: наркотики, насилие). Порнография отдельно обрабатывается NSFW-моделью на шаге 1;
    • считает вероятности соответствия изображения каждой метке;
    • собирает метки, превышающие 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/

Нецензурная лексика (profanity/):

  • dictionary.py — ProfanityDictionary загружает словарь из текстового файла в множество (set), убирает BOM и пустые строки, приводит к нижнему регистру. Метод contains(word) проверяет точное вхождение.
  • lemmatizer.py — Lemmatizer на базе pymorphy3.MorphAnalyzer, метод normalize(word) возвращает нормальную форму (лемму) слова.
  • detector.py — ProfanityDetector.detect(text):
    1. регулярным выражением [а-яА-ЯёЁ]+ извлекает русскоязычные слова (после приведения к нижнему регистру);
    2. каждое слово лемматизирует;
    3. проверяет, начинается ли лемма с какого-либо слова из словаря (префиксное сравнение через startswith);
    4. возвращает список найденных слов в исходном виде.

Валидация изображений (image/validator.py):

ImageValidator.validate(image) проверяет размеры изображения и выбрасывает InvalidImageException с описанием проблемы, если:

  • ширина/высота меньше MIN_WIDTH/MIN_HEIGHT (50);
  • ширина/высота больше MAX_WIDTH/MAX_HEIGHT (10000).

Логика модерации текста

TextModerationService.moderate(text) (app/services/text_moderation_service.py):

  1. Проверка входа. Если текст None или состоит из пробелов — выбрасывается InvalidTextException("Text is empty") → HTTP 400.
  2. Детектор нецензурной лексики. profanity_detector.detect(text):
    • если найдены запрещённые слова, сразу возвращается PredictionResult(label="PROFANITY", score=1.0, approved=False, reason="PROFANITY", detected_words=[...]) — нейросеть не вызывается (экономия ресурсов);
  3. Нейросетевая оценка. Иначе вызывается TextClassifier.predict(text):
    • approved = score < TEXT_TOXIC_THRESHOLD;
    • reason = "OK" при одобрении, иначе "TOXIC".

Итоговое решение передаётся в роутер и маппится в ModerationResponse.


Логика модерации изображений

ImageModerationService.moderate(image) (app/services/image_moderation_service.py):

  1. Валидация. ImageValidator.validate(image) — проверка размеров (см. выше).
  2. Шаг 1 — NSFW-модель. ImageClassifier.predict(image):
    • если метка nsfw и уверенность ≥ NSFW_THRESHOLD (0.85) → решение отклонить: reason="NSFW", detected_labels=["nsfw"];
  3. Шаг 2 — детектор оружия. WeaponDetector.predict(image):
    • 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=[...];
  5. Шаг 4 — «нормальное» изображение. Если ни одно из правил не сработало → approved=True, reason="OK", label="normal", score=1.0.

Таким образом, изображение одобряется только если оно прошло и NSFW-модель, и детектор оружия, и CLIP-проверку по запрещённым категориям.


Обработка ошибок

Класс Наследует Назначение
ModerationException Exception Базовый класс, хранит поле message
InvalidTextException ModerationException Некорректный текст (пустой)
InvalidImageException ModerationException Некорректное изображение (размеры)

Обработчики в app/exceptions/handlers.py преобразуют исключения в HTTP-ответ:

Исключение HTTP-код code в теле
InvalidTextException 400 INVALID_TEXT
InvalidImageException 400 INVALID_IMAGE

Тело ошибки всегда в формате ErrorResponse:

{
  "code": "INVALID_IMAGE",
  "message": "Image width is too small: 10"
}

Ошибки загрузки файла (формат/размер) обрабатываются в роутере напрямую через HTTPException с соответствующими кодами (400, 413).


Диагностика и логирование

  • Логи пишутся в stdout с уровнем INFO в формате время уровень сообщение.
  • При старте логируются шаги загрузки моделей и выбранное устройство.
  • В классификаторах присутствуют отладочные print-вызовы:
    • TextClassifier печатает входные тензоры, логиты и вероятности;
    • ImageClassifier печатает метки, предсказание и вероятности;
    • ClipClassifier печатает выбранное устройство при инициализации.

Известные особенности

  1. Неполный requirements.txt. В файл не включены ML-зависимости (torch, transformers, Pillow, pymorphy3), хотя они обязательны для работы. Для воспроизводимой установки их следует добавить в requirements.txt.
  2. HF_TOKEN зашит по умолчанию. Токен присутствует и в .env, и как значение по умолчанию в settings.py. Его необходимо заменить/вычистить перед публикацией проекта.
  3. Отладочный вывод. Классификаторы содержат print()-вызовы; в «боевом» режиме их стоит заменить на логирование через logger.
  4. Дублирование DI. Логика внедрения зависимостей дублируется в app/container.py (не используется) и в app/core/dependencies.py (основной путь).
  5. Производительность. Модели загружаются в память целиком; на CPU инференс CLIP-модели может быть медленным. Загрузка моделей происходит при старте приложения (см. lifespan), первое обращение к CLIP-эндпоинту также выполняет инициализацию модели.
Description
No description provided
Readme 91 KiB
Languages
Python 100%