AI Moderation Service
Сервис автоматической модерации пользовательского контента на базе FastAPI и моделей машинного обучения. Позволяет проверять тексты и изображения на наличие недопустимого контента:
- Тексты — токсичность (нейросетевая модель BERT) и нецензурная лексика (словарь + морфологический анализ
pymorphy3). - Изображения — NSFW-контент (нейросетевая модель
Falconsai/nsfw_image_detection) и запрещённые категории (марихуана, наркотики, оружие, порнография, насилие) через мультимодальную модель CLIP.
Сервис предоставляет REST API и возвращает вердикт (approved), оценку уверенности (score), причину отказа (reason) и дополнительную диагностическую информацию.
Оглавление
- Возможности
- Технологический стек
- Структура проекта
- Установка и запуск
- Конфигурация
- API
- Модели данных
- Архитектура и слои
- Логика модерации текста
- Логика модерации изображений
- Обработка ошибок
- Диагностика и логирование
- Известные особенности
Возможности
- Модерация текста:
- Проверка на нецензурную лексику по словарю из ~7 350 слов (включая транслит и цифро-замены вроде «6ля»);
- Лемматизация русских слов через
pymorphy3для поиска всех словоформ; - Нейросетевая оценка токсичности мультиязычной моделью
textdetox/bert-multilingual-toxicity-classifier; - Настраиваемый порог токсичности (по умолчанию
0.90).
- Модерация изображений:
- Проверка формата (JPEG/PNG/WebP) и размера файла (до 5 МБ);
- Валидация геометрии изображения (50×50 … 10000×10000 пикселей);
- Двухэтапная проверка: модель NSFW-классификации + CLIP-классификация по заранее заданным категориям;
- Комбинированный вердикт на основе нескольких моделей.
- Готовый 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) |
| Морфология русского языка | 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-классификации
│ ├── 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
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. Проверка работоспособности
- Интерактивная документация API: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
Конфигурация
Настройки определяются классом 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-классификатора |
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
При старте:
- Если задан
HF_TOKEN, он экспортируется в переменную окруженияHF_TOKEN; - Вызывается
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 marijuanaa photo containing drugsa photo containing weaponsa pornographic photoa photo containing violence
DI-фабрики — app/core/dependencies.py
Использует @lru_cache, что превращает фабрики в синглтоны:
| Фабрика | Возвращает |
|---|---|
get_text_classifier |
TextClassifier |
get_clip_classifier |
ClipClassifier |
get_image_classifier |
ImageClassifier |
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(токсичный);
- токенизирует текст (макс. 512 токенов,
image/image_classifier.py—ImageClassifier.predict(image):- обрабатывает изображение процессором модели;
- возвращает
ImagePredictionResultс меткой изid2label(обычноnsfw/normal);
image/clip_classifier.py—ClipClassifier:- загружает CLIP-модель и процессор сразу в конструкторе (при первом обращении);
- задаёт список из 6 текстовых меток (
a normal photo+ 5 запрещённых); - считает вероятности соответствия изображения каждой метке;
- собирает метки, превышающие
CLIP_THRESHOLD, и возвращаетClipPredictionResult.
Слой правил — app/moderation/
Нецензурная лексика (profanity/):
dictionary.py—ProfanityDictionaryзагружает словарь из текстового файла в множество (set), убирает BOM и пустые строки, приводит к нижнему регистру. Методcontains(word)проверяет точное вхождение.lemmatizer.py—Lemmatizerна базеpymorphy3.MorphAnalyzer, методnormalize(word)возвращает нормальную форму (лемму) слова.detector.py—ProfanityDetector.detect(text):- регулярным выражением
[а-яА-ЯёЁ]+извлекает русскоязычные слова (после приведения к нижнему регистру); - каждое слово лемматизирует;
- проверяет, начинается ли лемма с какого-либо слова из словаря (префиксное сравнение через
startswith); - возвращает список найденных слов в исходном виде.
- регулярным выражением
Валидация изображений (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):
- Проверка входа. Если текст
Noneили состоит из пробелов — выбрасываетсяInvalidTextException("Text is empty")→ HTTP 400. - Детектор нецензурной лексики.
profanity_detector.detect(text):- если найдены запрещённые слова, сразу возвращается
PredictionResult(label="PROFANITY", score=1.0, approved=False, reason="PROFANITY", detected_words=[...])— нейросеть не вызывается (экономия ресурсов);
- если найдены запрещённые слова, сразу возвращается
- Нейросетевая оценка. Иначе вызывается
TextClassifier.predict(text):approved = score < TEXT_TOXIC_THRESHOLD;reason = "OK"при одобрении, иначе"TOXIC".
Итоговое решение передаётся в роутер и маппится в ModerationResponse.
Логика модерации изображений
ImageModerationService.moderate(image) (app/services/image_moderation_service.py):
- Валидация.
ImageValidator.validate(image)— проверка размеров (см. выше). - Шаг 1 — NSFW-модель.
ImageClassifier.predict(image):- если метка
nsfwи уверенность ≥NSFW_THRESHOLD(0.85) → решение отклонить:reason="NSFW",detected_labels=["nsfw"];
- если метка
- Шаг 2 — CLIP-модель.
ClipClassifier.predict(image):- из меток, превысивших
CLIP_THRESHOLD, отбираются только те, что входят вFORBIDDEN_IMAGE_LABELS; - если такие категории найдены → решение отклонить:
reason="FORBIDDEN_CONTENT",detected_labels=[...];
- из меток, превысивших
- Шаг 3 — «нормальное» изображение. Если ни одно из правил не сработало →
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печатает выбранное устройство при инициализации.
Известные особенности
- Неполный
requirements.txt. В файл не включены ML-зависимости (torch,transformers,Pillow,pymorphy3), хотя они обязательны для работы. Для воспроизводимой установки их следует добавить вrequirements.txt. HF_TOKENзашит по умолчанию. Токен присутствует и в.env, и как значение по умолчанию вsettings.py. Его необходимо заменить/вычистить перед публикацией проекта.- Отладочный вывод. Классификаторы содержат
print()-вызовы; в «боевом» режиме их стоит заменить на логирование черезlogger. - Дублирование DI. Логика внедрения зависимостей дублируется в
app/container.py(не используется) и вapp/core/dependencies.py(основной путь). - Производительность. Модели загружаются в память целиком; на CPU инференс CLIP-модели может быть медленным. Загрузка моделей происходит при старте приложения (см.
lifespan), первое обращение к CLIP-эндпоинту также выполняет инициализацию модели.